From ede858b8dce8bf077cf846acb87be19d2ee8f785 Mon Sep 17 00:00:00 2001 From: victrixhominum Date: Wed, 9 Jul 2025 15:41:18 +0100 Subject: [PATCH 01/15] added husky precommit hook --- .husky/_/husky.sh | 32 + .husky/pre-commit | 8 + package-lock.json | 1439 +++++++++++++++++++++++++++++++++++++++++++-- package.json | 11 +- 4 files changed, 1455 insertions(+), 35 deletions(-) create mode 100755 .husky/_/husky.sh create mode 100755 .husky/pre-commit diff --git a/.husky/_/husky.sh b/.husky/_/husky.sh new file mode 100755 index 0000000..a09c6ca --- /dev/null +++ b/.husky/_/husky.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env sh +if [ -z "$husky_skip_init" ]; then + debug () { + if [ "$HUSKY_DEBUG" = "1" ]; then + echo "husky (debug) - $1" + fi + } + + readonly hook_name="$(basename -- "$0")" + debug "starting $hook_name..." + + if [ "$HUSKY" = "0" ]; then + debug "HUSKY env variable is set to 0, skipping hook" + exit 0 + fi + + if [ -f ~/.huskyrc ]; then + debug "sourcing ~/.huskyrc" + . ~/.huskyrc + fi + + readonly husky_skip_init=1 + export husky_skip_init + sh -e "$0" "$@" + exitCode="$?" + + if [ $exitCode != 0 ]; then + echo "husky - $hook_name hook exited with code $exitCode (error)" + fi + + exit $exitCode +fi diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..b1519b6 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,8 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +# Run lint-staged to lint staged files +npx lint-staged + +# Run tests +npm test diff --git a/package-lock.json b/package-lock.json index 0da2244..cc13e34 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,8 +37,11 @@ "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "15.3.2", + "husky": "^8.0.3", "jest": "^30.0.4", "jest-environment-jsdom": "^30.0.4", + "lint-staged": "^15.2.0", + "nyc": "^15.1.0", "prisma": "^6.8.2", "tailwind-merge": "^3.3.0", "tailwindcss": "^4", @@ -4186,6 +4189,19 @@ "node": ">= 14" } }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -4259,6 +4275,24 @@ "node": ">= 8" } }, + "node_modules/append-transform": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", + "dev": true, + "dependencies": { + "default-require-extensions": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", + "dev": true + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -4699,6 +4733,63 @@ "node": ">=10.16.0" } }, + "node_modules/caching-transform": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", + "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", + "dev": true, + "dependencies": { + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/caching-transform/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caching-transform/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/caching-transform/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/caching-transform/node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -4883,6 +4974,69 @@ "integrity": "sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==", "dev": true }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -5031,6 +5185,27 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -5233,6 +5408,15 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/decimal.js": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", @@ -5269,6 +5453,30 @@ "node": ">=0.10.0" } }, + "node_modules/default-require-extensions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", + "integrity": "sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==", + "dev": true, + "dependencies": { + "strip-bom": "^4.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-require-extensions/node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -5578,6 +5786,18 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -5767,6 +5987,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, "node_modules/esbuild": { "version": "0.25.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", @@ -6269,6 +6495,12 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -6420,6 +6652,47 @@ "node": ">=8" } }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-cache-dir/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-cache-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -6490,6 +6763,26 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/fromentries": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", + "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -6570,6 +6863,18 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -6856,6 +7161,31 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hasha": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", + "dev": true, + "dependencies": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hasha/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -6941,6 +7271,21 @@ "node": ">=10.17.0" } }, + "node_modules/husky": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz", + "integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==", + "dev": true, + "bin": { + "husky": "lib/bin.js" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -7435,6 +7780,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true + }, "node_modules/is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -7481,6 +7832,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -7504,6 +7864,18 @@ "node": ">=8" } }, + "node_modules/istanbul-lib-hook": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", + "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", + "dev": true, + "dependencies": { + "append-transform": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/istanbul-lib-instrument": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", @@ -7520,16 +7892,33 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "node_modules/istanbul-lib-processinfo": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz", + "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==", "dev": true, "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, + "archy": "^1.0.0", + "cross-spawn": "^7.0.3", + "istanbul-lib-coverage": "^3.2.0", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, "engines": { "node": ">=10" } @@ -8939,6 +9328,18 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -8974,6 +9375,236 @@ "integrity": "sha512-DRSlB9DKVW04c4SUdGvKK5FR6be45lTU9M76JnngqPeeGDqPwYc0zdUErtsNVMtxPXgUWV4HbXbnC4sNyBxkYg==", "license": "MIT" }, + "node_modules/lint-staged": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.5.2.tgz", + "integrity": "sha512-YUSOLq9VeRNAo/CTaVmhGDKG+LBtA8KF1X4K5+ykMSwWST1vDxJRB2kv2COgLb1fvpCo+A/y9A0G0znNVmdx4w==", + "dev": true, + "dependencies": { + "chalk": "^5.4.1", + "commander": "^13.1.0", + "debug": "^4.4.0", + "execa": "^8.0.1", + "lilconfig": "^3.1.3", + "listr2": "^8.2.5", + "micromatch": "^4.0.8", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.7.0" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/lint-staged/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/lint-staged/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/lint-staged/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/lint-staged/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/lint-staged/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/lint-staged/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/lint-staged/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/lint-staged/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/lint-staged/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/listr2": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.3.3.tgz", + "integrity": "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==", + "dev": true, + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true + }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -8996,6 +9627,12 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, + "node_modules/lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -9003,6 +9640,123 @@ "dev": true, "license": "MIT" }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "dev": true, + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "dev": true, + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -9154,6 +9908,18 @@ "node": ">=6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -9410,51 +10176,371 @@ "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "dev": true }, - "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "node_modules/node-preload": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", + "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", + "dev": true, + "dependencies": { + "process-on-spawn": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nwsapi": { + "version": "2.2.20", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", + "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", + "dev": true + }, + "node_modules/nyc": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", + "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==", + "dev": true, + "dependencies": { + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "caching-transform": "^4.0.0", + "convert-source-map": "^1.7.0", + "decamelize": "^1.2.0", + "find-cache-dir": "^3.2.0", + "find-up": "^4.1.0", + "foreground-child": "^2.0.0", + "get-package-type": "^0.1.0", + "glob": "^7.1.6", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-hook": "^3.0.0", + "istanbul-lib-instrument": "^4.0.0", + "istanbul-lib-processinfo": "^2.0.2", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "make-dir": "^3.0.0", + "node-preload": "^0.2.1", + "p-map": "^3.0.0", + "process-on-spawn": "^1.0.0", + "resolve-from": "^5.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "spawn-wrap": "^2.0.0", + "test-exclude": "^6.0.0", + "yargs": "^15.0.2" + }, + "bin": { + "nyc": "bin/nyc.js" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/nyc/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/nyc/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/nyc/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/nyc/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/nyc/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/nyc/node_modules/istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/nyc/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nyc/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nyc/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/nyc/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "node_modules/nyc/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "node_modules/nyc/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "dependencies": { - "path-key": "^3.0.0" + "ansi-regex": "^5.0.1" }, "engines": { "node": ">=8" } }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "license": "BSD-2-Clause", + "node_modules/nyc/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, "dependencies": { - "boolbase": "^1.0.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" + "engines": { + "node": ">=8" } }, - "node_modules/nwsapi": { - "version": "2.2.20", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", - "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", + "node_modules/nyc/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", "dev": true }, + "node_modules/nyc/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/oauth": { "version": "0.9.15", "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", @@ -9719,6 +10805,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -9728,6 +10826,21 @@ "node": ">=6" } }, + "node_modules/package-hash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", + "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.15", + "hasha": "^5.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -9879,6 +10992,18 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/pirates": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", @@ -10055,6 +11180,18 @@ } } }, + "node_modules/process-on-spawn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.1.0.tgz", + "integrity": "sha512-JOnOPQ/8TZgjs1JIH/m9ni7FfimjNa/PRx7y/Wb5qdItsnhO0jE4AT7fC0HjC28DUQWDr50dwSYZLdRMlqDq3Q==", + "dev": true, + "dependencies": { + "fromentries": "^1.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -10208,6 +11345,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==", + "dev": true, + "dependencies": { + "es6-error": "^4.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -10217,6 +11366,12 @@ "node": ">=0.10.0" } }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -10279,6 +11434,37 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -10290,6 +11476,49 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/rrweb-cssom": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", @@ -10412,6 +11641,12 @@ "node": ">=10" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -10633,6 +11868,46 @@ "node": ">=8" } }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/socket.io": { "version": "4.8.1", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", @@ -10785,6 +12060,66 @@ "source-map": "^0.6.0" } }, + "node_modules/spawn-wrap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", + "dev": true, + "dependencies": { + "foreground-child": "^2.0.0", + "is-windows": "^1.0.2", + "make-dir": "^3.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "which": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/spawn-wrap/node_modules/foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/spawn-wrap/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/spawn-wrap/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/spawn-wrap/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -10827,6 +12162,15 @@ "node": ">=10.0.0" } }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -11578,6 +12922,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", @@ -11892,6 +13245,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "dev": true + }, "node_modules/which-typed-array": { "version": "1.1.19", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", @@ -12094,6 +13453,18 @@ "node": ">=18" } }, + "node_modules/yaml": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/package.json b/package.json index 684a790..33318ef 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,13 @@ "start": "node -r dotenv/config server.js dotenv_config_path=.env.production", "lint": "next lint", "test": "jest", - "test:watch": "jest --watch" + "test:watch": "jest --watch", + "prepare": "husky" + }, + "lint-staged": { + "*.{js,jsx,ts,tsx}": [ + "eslint --fix" + ] }, "dependencies": { "@auth/prisma-adapter": "^2.9.1", @@ -41,8 +47,11 @@ "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "15.3.2", + "husky": "^8.0.3", "jest": "^30.0.4", "jest-environment-jsdom": "^30.0.4", + "lint-staged": "^15.2.0", + "nyc": "^15.1.0", "prisma": "^6.8.2", "tailwind-merge": "^3.3.0", "tailwindcss": "^4", From 72aed7dd536b9b680ee1b2720d1c070c9cb3346a Mon Sep 17 00:00:00 2001 From: victrixhominum Date: Wed, 9 Jul 2025 15:43:42 +0100 Subject: [PATCH 02/15] added docs on husky precommit --- docs/CONTRIBUTING.md | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index d025678..dd3f343 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -29,32 +29,52 @@ cd yapli 2. Set up environment variables into `.env.local` (see `.env.example`) 3. Start development server: `npm run dev` -### 4. Create a Branch +### 4. Pre-commit Hooks with Husky + +Yapli uses [Husky](https://typicode.github.io/husky/) to run pre-commit hooks that ensure code quality before commits: + +1. **Automatic Setup**: Husky is automatically installed and configured when you run `npm install` (via the `prepare` script) +2. **Pre-commit Hook**: The pre-commit hook runs: + - `lint-staged`: Runs ESLint on staged JavaScript/TypeScript files + - `npm test`: Runs all tests to ensure nothing breaks + +If you're not seeing the pre-commit hooks run when you commit: + +1. Ensure Git hooks are enabled: `git config core.hooksPath .husky` +2. Make sure Husky is installed: `npm run prepare` +3. Verify the pre-commit hook is executable: `chmod +x .husky/pre-commit` + +To temporarily bypass the pre-commit hook (not recommended): +```bash +git commit -m "Your message" --no-verify +``` + +### 5. Create a Branch ```bash git checkout -b your-branch-name ``` -### 5. Make Your Changes +### 6. Make Your Changes - Follow the code standards outlined in [CLAUDE.md](CLAUDE.md) - Write tests for new functionality - Ensure your code passes linting: `npm run lint` and `npm run build` -### 6. Commit Your Changes +### 7. Commit Your Changes ```bash git add . git commit -m "feat: add your feature description" ``` -### 7. Push to Your Fork +### 8. Push to Your Fork ```bash git push origin your-branch-name ``` -### 8. Create a Pull Request +### 9. Create a Pull Request 1. Navigate to your fork on GitHub 2. Click "New Pull Request" From 4a36fda85be433100d9ad72e72bba0d4781adba5 Mon Sep 17 00:00:00 2001 From: victrixhominum Date: Wed, 9 Jul 2025 13:41:51 +0100 Subject: [PATCH 03/15] added unit tests for components --- test/components/AliasModal.test.tsx | 126 ++++++++++++++++++++ test/components/Brand.test.tsx | 49 ++++++++ test/components/CopyUrlButton.test.tsx | 76 ++++++++++++ test/components/DeleteRoomButton.test.tsx | 58 +++++++++ test/components/FormInput.test.tsx | 84 +++++++++++++ test/components/Logo.test.tsx | 89 ++++++++++++++ test/components/LogoMark.test.tsx | 36 ++++++ test/components/ThemeToggle.test.tsx | 138 ++++++++++++++++++++++ 8 files changed, 656 insertions(+) create mode 100644 test/components/AliasModal.test.tsx create mode 100644 test/components/Brand.test.tsx create mode 100644 test/components/CopyUrlButton.test.tsx create mode 100644 test/components/DeleteRoomButton.test.tsx create mode 100644 test/components/FormInput.test.tsx create mode 100644 test/components/Logo.test.tsx create mode 100644 test/components/LogoMark.test.tsx create mode 100644 test/components/ThemeToggle.test.tsx diff --git a/test/components/AliasModal.test.tsx b/test/components/AliasModal.test.tsx new file mode 100644 index 0000000..887f297 --- /dev/null +++ b/test/components/AliasModal.test.tsx @@ -0,0 +1,126 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import AliasModal from '@/components/AliasModal'; +import * as aliasStorage from '@/lib/aliasStorage'; + +// Mock the aliasStorage module +jest.mock('../../src/lib/aliasStorage', () => ({ + saveAlias: jest.fn(), +})); + +describe('AliasModal', () => { + const mockOnAliasSet = jest.fn(); + const mockRoomId = 'room123'; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders correctly when isOpen is true', () => { + render( + + ); + + expect(screen.getByText('Join Chat Room')).toBeInTheDocument(); + expect(screen.getByLabelText(/Enter your name to join the chat:/i)).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /Join Chat/i })).toBeInTheDocument(); + }); + + it('does not render when isOpen is false', () => { + render( + + ); + + expect(screen.queryByText('Join Chat Room')).not.toBeInTheDocument(); + }); + + it('disables submit button when input is empty', () => { + render( + + ); + + const button = screen.getByRole('button', { name: /Join Chat/i }); + expect(button).toBeDisabled(); + }); + + it('enables submit button when input has value', () => { + render( + + ); + + const input = screen.getByLabelText(/Enter your name to join the chat:/i); + fireEvent.change(input, { target: { value: 'TestUser' } }); + + const button = screen.getByRole('button', { name: /Join Chat/i }); + expect(button).not.toBeDisabled(); + }); + + it('displays error message when error prop is provided', () => { + const errorMessage = 'Error message'; + render( + + ); + + expect(screen.getByText(errorMessage)).toBeInTheDocument(); + }); + + it('calls onAliasSet and saveAlias when form is submitted with valid input', () => { + render( + + ); + + const input = screen.getByLabelText(/Enter your name to join the chat:/i); + fireEvent.change(input, { target: { value: 'TestUser' } }); + + const button = screen.getByRole('button', { name: /Join Chat/i }); + fireEvent.click(button); + + expect(aliasStorage.saveAlias).toHaveBeenCalledWith(mockRoomId, 'TestUser'); + expect(mockOnAliasSet).toHaveBeenCalledWith('TestUser'); + }); + + it('trims whitespace from alias before saving and submitting', () => { + render( + + ); + + const input = screen.getByLabelText(/Enter your name to join the chat:/i); + fireEvent.change(input, { target: { value: ' TestUser ' } }); + + const button = screen.getByRole('button', { name: /Join Chat/i }); + fireEvent.click(button); + + expect(aliasStorage.saveAlias).toHaveBeenCalledWith(mockRoomId, 'TestUser'); + expect(mockOnAliasSet).toHaveBeenCalledWith('TestUser'); + }); +}); diff --git a/test/components/Brand.test.tsx b/test/components/Brand.test.tsx new file mode 100644 index 0000000..d0abb8e --- /dev/null +++ b/test/components/Brand.test.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import Brand from '@/components/Brand'; + +// Mock the Logo component since we're testing Brand in isolation +jest.mock('../../src/components/Logo', () => { + return function MockLogo({ size, className }: { size: number; className: string }) { + return
; + }; +}); + +describe('Brand', () => { + it('renders the brand name correctly', () => { + render(); + + // Check if the brand name is rendered + expect(screen.getByText('yapli')).toBeInTheDocument(); + }); + + it('renders the Logo component with correct props', () => { + render(); + + // Check if the Logo component is rendered with correct props + const logo = screen.getByTestId('mock-logo'); + expect(logo).toBeInTheDocument(); + expect(logo).toHaveAttribute('data-size', '32'); + expect(logo).toHaveAttribute('class', 'mt-2'); + }); + + it('applies correct styling to the brand name', () => { + render(); + + const brandName = screen.getByText('yapli'); + expect(brandName).toHaveClass('font-bold'); + expect(brandName).toHaveClass('font-mono'); + expect(brandName).toHaveClass('text-yapli-teal'); + }); + + it('has a flex container with correct styling', () => { + const { container } = render(); + + // Get the main container div + const brandContainer = container.firstChild; + expect(brandContainer).toHaveClass('flex'); + expect(brandContainer).toHaveClass('items-center'); + expect(brandContainer).toHaveClass('gap-2'); + }); +}); diff --git a/test/components/CopyUrlButton.test.tsx b/test/components/CopyUrlButton.test.tsx new file mode 100644 index 0000000..510d446 --- /dev/null +++ b/test/components/CopyUrlButton.test.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import CopyUrlButton from '@/components/CopyUrlButton'; + +// Mock the heroicons +jest.mock('@heroicons/react/24/outline', () => ({ + LinkIcon: () =>
, +})); + +describe('CopyUrlButton', () => { + const defaultProps = { + onClick: jest.fn(), + roomTitle: 'Test Room', + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders correctly with default props', () => { + render(); + + expect(screen.getByRole('button')).toBeInTheDocument(); + expect(screen.getByTestId('link-icon')).toBeInTheDocument(); + expect(screen.queryByText('Copy URL')).not.toBeInTheDocument(); + }); + + it('calls onClick when clicked', () => { + render(); + + fireEvent.click(screen.getByRole('button')); + + expect(defaultProps.onClick).toHaveBeenCalledTimes(1); + }); + + it('renders with text when showText is true', () => { + render(); + + expect(screen.getByText('Copy URL')).toBeInTheDocument(); + }); + + it('applies fullWidth class when fullWidth is true', () => { + render(); + + expect(screen.getByRole('button')).toHaveClass('w-full'); + }); + + it('applies custom className when provided', () => { + render(); + + expect(screen.getByRole('button').parentElement).toHaveClass('custom-class'); + }); + + it('has correct aria-label with room title', () => { + render(); + + expect(screen.getByRole('button')).toHaveAttribute('aria-label', 'Copy URL for Test Room chatroom'); + }); + + it('renders tooltip text', () => { + render(); + + expect(screen.getByText('Copy full URL to clipboard')).toBeInTheDocument(); + }); + + it('applies correct classes for button with text', () => { + render(); + + const button = screen.getByRole('button'); + expect(button).toHaveClass('flex'); + expect(button).toHaveClass('items-center'); + expect(button).toHaveClass('justify-center'); + expect(button).toHaveClass('text-base'); + }); +}); diff --git a/test/components/DeleteRoomButton.test.tsx b/test/components/DeleteRoomButton.test.tsx new file mode 100644 index 0000000..7ffd83f --- /dev/null +++ b/test/components/DeleteRoomButton.test.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import DeleteRoomButton from '@/components/DeleteRoomButton'; + +// Mock the heroicons +jest.mock('@heroicons/react/24/outline', () => ({ + TrashIcon: () =>
, +})); + +describe('DeleteRoomButton', () => { + const defaultProps = { + onClick: jest.fn(), + roomTitle: 'Test Room', + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders correctly with default props', () => { + render(); + + expect(screen.getByRole('button')).toBeInTheDocument(); + expect(screen.getByTestId('trash-icon')).toBeInTheDocument(); + }); + + it('calls onClick when clicked', () => { + render(); + + fireEvent.click(screen.getByRole('button')); + + expect(defaultProps.onClick).toHaveBeenCalledTimes(1); + }); + + it('has correct aria-label with room title', () => { + render(); + + expect(screen.getByRole('button')).toHaveAttribute('aria-label', 'Delete Test Room chatroom'); + }); + + it('has correct title attribute', () => { + render(); + + expect(screen.getByRole('button')).toHaveAttribute('title', 'Delete room'); + }); + + it('has correct styling classes', () => { + render(); + + const button = screen.getByRole('button'); + expect(button).toHaveClass('bg-red-500'); + expect(button).toHaveClass('text-white'); + expect(button).toHaveClass('rounded-md'); + expect(button).toHaveClass('hover:bg-red-400'); + expect(button).toHaveClass('transition-colors'); + }); +}); diff --git a/test/components/FormInput.test.tsx b/test/components/FormInput.test.tsx new file mode 100644 index 0000000..f1d8e22 --- /dev/null +++ b/test/components/FormInput.test.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import FormInput from '@/components/FormInput'; + +describe('FormInput', () => { + const defaultProps = { + type: 'text', + id: 'test-input', + label: 'Test Label', + value: '', + onChange: jest.fn(), + }; + + it('renders correctly with default props', () => { + render(); + + expect(screen.getByLabelText('Test Label')).toBeInTheDocument(); + expect(screen.getByLabelText('Test Label')).toHaveAttribute('type', 'text'); + expect(screen.getByLabelText('Test Label')).toHaveAttribute('id', 'test-input'); + expect(screen.getByLabelText('Test Label')).not.toBeRequired(); + expect(screen.getByLabelText('Test Label')).not.toBeDisabled(); + }); + + it('renders with the provided value', () => { + render(); + + expect(screen.getByLabelText('Test Label')).toHaveValue('Test Value'); + }); + + it('calls onChange when the input value changes', () => { + const handleChange = jest.fn(); + render(); + + fireEvent.change(screen.getByLabelText('Test Label'), { target: { value: 'New Value' } }); + + expect(handleChange).toHaveBeenCalledTimes(1); + }); + + it('applies required attribute when required prop is true', () => { + render(); + + expect(screen.getByLabelText('Test Label')).toBeRequired(); + }); + + it('applies placeholder when provided', () => { + render(); + + expect(screen.getByLabelText('Test Label')).toHaveAttribute('placeholder', 'Test Placeholder'); + }); + + it('applies maxLength when provided', () => { + render(); + + expect(screen.getByLabelText('Test Label')).toHaveAttribute('maxLength', '10'); + }); + + it('disables the input when disabled prop is true', () => { + render(); + + expect(screen.getByLabelText('Test Label')).toBeDisabled(); + }); + + it('applies custom className when provided', () => { + render(); + + expect(screen.getByLabelText('Test Label')).toHaveClass('custom-class'); + }); + + it('calls onKeyPress when a key is pressed', () => { + const handleKeyPress = jest.fn(); + render(); + + fireEvent.keyPress(screen.getByLabelText('Test Label'), { key: 'Enter', code: 'Enter' }); + + expect(handleKeyPress).toHaveBeenCalledTimes(1); + }); + + it('renders with different input types', () => { + render(); + + expect(screen.getByLabelText('Test Label')).toHaveAttribute('type', 'password'); + }); +}); diff --git a/test/components/Logo.test.tsx b/test/components/Logo.test.tsx new file mode 100644 index 0000000..854c9fe --- /dev/null +++ b/test/components/Logo.test.tsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import Logo from '@/components/Logo'; + +describe('Logo', () => { + it('renders with default props', () => { + const { container } = render(); + + // Check if the SVG is rendered + const svg = container.querySelector('svg'); + expect(svg).toBeInTheDocument(); + + // Check default size + expect(svg).toHaveAttribute('width', '48'); + + // Check if the logo body and eyes are present + expect(container.querySelector('.logo-body')).toBeInTheDocument(); + expect(container.querySelector('.logo-eyes')).toBeInTheDocument(); + }); + + it('applies custom size', () => { + const { container } = render(); + + const svg = container.querySelector('svg'); + expect(svg).toHaveAttribute('width', '100'); + + // Height should be proportional to width (595/500 ratio) + expect(svg).toHaveAttribute('height', '119'); + }); + + it('applies custom body color', () => { + const { container } = render(); + + const style = container.querySelector('style'); + expect(style?.textContent).toContain('fill: #FF0000'); + }); + + it('applies custom eye color', () => { + const { container } = render(); + + const style = container.querySelector('style'); + expect(style?.textContent).toContain('fill: #00FF00'); + }); + + it('applies hover colors', () => { + const { container } = render( + + ); + + const style = container.querySelector('style'); + expect(style?.textContent).toContain('fill: #0000FF'); + expect(style?.textContent).toContain('fill: #FFFF00'); + }); + + it('applies animation class when animate is true', () => { + const { container } = render(); + + const svg = container.querySelector('svg'); + expect(svg).toHaveClass('transition-all'); + expect(svg).toHaveClass('duration-300'); + expect(svg).toHaveClass('ease-in-out'); + expect(svg).toHaveClass('hover:scale-105'); + }); + + it('does not apply animation class when animate is false', () => { + const { container } = render(); + + const svg = container.querySelector('svg'); + expect(svg).not.toHaveClass('transition-all'); + expect(svg).not.toHaveClass('duration-300'); + expect(svg).not.toHaveClass('ease-in-out'); + expect(svg).not.toHaveClass('hover:scale-105'); + }); + + it('applies custom className', () => { + const { container } = render(); + + const svg = container.querySelector('svg'); + expect(svg).toHaveClass('custom-class'); + }); + + it('has aria-hidden attribute', () => { + const { container } = render(); + + const svg = container.querySelector('svg'); + expect(svg).toHaveAttribute('aria-hidden', 'true'); + }); +}); diff --git a/test/components/LogoMark.test.tsx b/test/components/LogoMark.test.tsx new file mode 100644 index 0000000..5126374 --- /dev/null +++ b/test/components/LogoMark.test.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import LogoMark from '@/components/LogoMark'; + +// Mock the Logo component since we're testing LogoMark in isolation +jest.mock('../../src/components/Logo', () => { + return function MockLogo({ size, className, animate }: { size: number; className: string; animate: boolean }) { + return
; + }; +}); + +describe('LogoMark', () => { + it('renders the Logo component with correct props', () => { + const { getByTestId } = render(); + + const logo = getByTestId('mock-logo'); + expect(logo).toBeInTheDocument(); + expect(logo).toHaveAttribute('data-size', '48'); + expect(logo).toHaveAttribute('data-animate', 'true'); + expect(logo).toHaveAttribute('class', 'rounded-lg'); + }); + + it('has a fixed position container with correct styling', () => { + const { container } = render(); + + const logoMarkContainer = container.firstChild; + expect(logoMarkContainer).toHaveClass('fixed'); + expect(logoMarkContainer).toHaveClass('bottom-4'); + expect(logoMarkContainer).toHaveClass('right-4'); + expect(logoMarkContainer).toHaveClass('z-10'); + expect(logoMarkContainer).toHaveClass('opacity-60'); + expect(logoMarkContainer).toHaveClass('hover:opacity-100'); + expect(logoMarkContainer).toHaveClass('transition-opacity'); + }); +}); diff --git a/test/components/ThemeToggle.test.tsx b/test/components/ThemeToggle.test.tsx new file mode 100644 index 0000000..0701862 --- /dev/null +++ b/test/components/ThemeToggle.test.tsx @@ -0,0 +1,138 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import ThemeToggle from '@/components/ThemeToggle'; + +// Mock the next-themes library +jest.mock('next-themes', () => ({ + useTheme: jest.fn(), +})); + +// Mock the heroicons +jest.mock('@heroicons/react/24/outline', () => ({ + SunIcon: () =>
, + MoonIcon: () =>
, +})); + +import { useTheme } from 'next-themes'; + +describe('ThemeToggle', () => { + beforeEach(() => { + jest.clearAllMocks(); + + // Default mock implementation + (useTheme as jest.Mock).mockReturnValue({ + theme: 'light', + setTheme: jest.fn(), + resolvedTheme: 'light', + }); + }); + + it('renders nothing when not mounted', () => { + // Override useEffect to not set mounted to true + jest.spyOn(React, 'useEffect').mockImplementationOnce(() => {}); + + const { container } = render(); + expect(container).toBeEmptyDOMElement(); + }); + + it('renders the moon icon when theme is light', () => { + (useTheme as jest.Mock).mockReturnValue({ + theme: 'light', + setTheme: jest.fn(), + resolvedTheme: 'light', + }); + + render(); + + expect(screen.getByTestId('moon-icon')).toBeInTheDocument(); + expect(screen.queryByTestId('sun-icon')).not.toBeInTheDocument(); + }); + + it('renders the sun icon when theme is dark', () => { + (useTheme as jest.Mock).mockReturnValue({ + theme: 'dark', + setTheme: jest.fn(), + resolvedTheme: 'dark', + }); + + render(); + + expect(screen.getByTestId('sun-icon')).toBeInTheDocument(); + expect(screen.queryByTestId('moon-icon')).not.toBeInTheDocument(); + }); + + it('toggles from light to dark when clicked', () => { + const setTheme = jest.fn(); + (useTheme as jest.Mock).mockReturnValue({ + theme: 'light', + setTheme, + resolvedTheme: 'light', + }); + + render(); + + fireEvent.click(screen.getByRole('button')); + + expect(setTheme).toHaveBeenCalledWith('dark'); + }); + + it('toggles from dark to light when clicked', () => { + const setTheme = jest.fn(); + (useTheme as jest.Mock).mockReturnValue({ + theme: 'dark', + setTheme, + resolvedTheme: 'dark', + }); + + render(); + + fireEvent.click(screen.getByRole('button')); + + expect(setTheme).toHaveBeenCalledWith('light'); + }); + + it('handles system theme correctly', () => { + const setTheme = jest.fn(); + (useTheme as jest.Mock).mockReturnValue({ + theme: 'system', + setTheme, + resolvedTheme: 'dark', // System is currently showing dark + }); + + render(); + + fireEvent.click(screen.getByRole('button')); + + // Should switch to light since system is showing dark + expect(setTheme).toHaveBeenCalledWith('light'); + }); + + it('has the correct tooltip text for light mode', () => { + (useTheme as jest.Mock).mockReturnValue({ + theme: 'light', + setTheme: jest.fn(), + resolvedTheme: 'light', + }); + + render(); + + const button = screen.getByRole('button'); + expect(button).toHaveAttribute('title', 'Switch to dark mode'); + expect(button).toHaveAttribute('aria-label', 'Switch to dark mode'); + }); + + it('has the correct tooltip text for dark mode', () => { + (useTheme as jest.Mock).mockReturnValue({ + theme: 'dark', + setTheme: jest.fn(), + resolvedTheme: 'dark', + }); + + render(); + + const button = screen.getByRole('button'); + expect(button).toHaveAttribute('title', 'Switch to light mode'); + expect(button).toHaveAttribute('aria-label', 'Switch to light mode'); + }); +}); From e0b8f0ca33b098ab12c9d9d62da1abd42296f63e Mon Sep 17 00:00:00 2001 From: victrixhominum Date: Wed, 9 Jul 2025 13:42:14 +0100 Subject: [PATCH 04/15] added tests for lib --- test/lib/aliasStorage.test.ts | 147 ++++++++++++++++++++++++++++++++++ test/lib/roomApi.test.ts | 146 +++++++++++++++++++++++++++++++++ test/lib/roomUtils.test.ts | 99 +++++++++++++++++++++++ 3 files changed, 392 insertions(+) create mode 100644 test/lib/aliasStorage.test.ts create mode 100644 test/lib/roomApi.test.ts create mode 100644 test/lib/roomUtils.test.ts diff --git a/test/lib/aliasStorage.test.ts b/test/lib/aliasStorage.test.ts new file mode 100644 index 0000000..4ae81ab --- /dev/null +++ b/test/lib/aliasStorage.test.ts @@ -0,0 +1,147 @@ +import { saveAlias, getAlias, removeAlias, isLocalStorageAvailable } from '@/lib/aliasStorage'; + +describe('aliasStorage utility', () => { + const mockRoomId = 'room123'; + const mockAlias = 'TestUser'; + const mockStorageKey = 'yapli_alias_room123'; + + // Mock localStorage + let localStorageMock: { + getItem: jest.Mock; + setItem: jest.Mock; + removeItem: jest.Mock; + clear: jest.Mock; + }; + + beforeEach(() => { + // Setup localStorage mock + localStorageMock = { + getItem: jest.fn(), + setItem: jest.fn(), + removeItem: jest.fn(), + clear: jest.fn(), + }; + + // Replace global localStorage with mock + Object.defineProperty(window, 'localStorage', { + value: localStorageMock, + writable: true + }); + }); + + describe('saveAlias', () => { + it('should save alias to localStorage with correct key', () => { + saveAlias(mockRoomId, mockAlias); + + expect(localStorageMock.setItem).toHaveBeenCalledWith(mockStorageKey, mockAlias); + }); + + it('should handle errors gracefully', () => { + // Mock console.warn to prevent test output noise + const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); + + // Make localStorage.setItem throw an error + localStorageMock.setItem.mockImplementation(() => { + throw new Error('Storage error'); + }); + + // Function should not throw + expect(() => saveAlias(mockRoomId, mockAlias)).not.toThrow(); + + // Should log warning + expect(consoleWarnSpy).toHaveBeenCalled(); + + consoleWarnSpy.mockRestore(); + }); + }); + + describe('getAlias', () => { + it('should retrieve alias from localStorage with correct key', () => { + localStorageMock.getItem.mockReturnValue(mockAlias); + + const result = getAlias(mockRoomId); + + expect(localStorageMock.getItem).toHaveBeenCalledWith(mockStorageKey); + expect(result).toBe(mockAlias); + }); + + it('should return null if alias is not found', () => { + localStorageMock.getItem.mockReturnValue(null); + + const result = getAlias(mockRoomId); + + expect(result).toBeNull(); + }); + + it('should handle errors gracefully', () => { + // Mock console.warn to prevent test output noise + const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); + + // Make localStorage.getItem throw an error + localStorageMock.getItem.mockImplementation(() => { + throw new Error('Storage error'); + }); + + // Function should not throw and return null + expect(getAlias(mockRoomId)).toBeNull(); + + // Should log warning + expect(consoleWarnSpy).toHaveBeenCalled(); + + consoleWarnSpy.mockRestore(); + }); + }); + + describe('removeAlias', () => { + it('should remove alias from localStorage with correct key', () => { + removeAlias(mockRoomId); + + expect(localStorageMock.removeItem).toHaveBeenCalledWith(mockStorageKey); + }); + + it('should handle errors gracefully', () => { + // Mock console.warn to prevent test output noise + const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); + + // Make localStorage.removeItem throw an error + localStorageMock.removeItem.mockImplementation(() => { + throw new Error('Storage error'); + }); + + // Function should not throw + expect(() => removeAlias(mockRoomId)).not.toThrow(); + + // Should log warning + expect(consoleWarnSpy).toHaveBeenCalled(); + + consoleWarnSpy.mockRestore(); + }); + }); + + describe('isLocalStorageAvailable', () => { + it('should return true when localStorage is available', () => { + expect(isLocalStorageAvailable()).toBe(true); + }); + + it('should return false when localStorage is not available', () => { + // Simulate environment without localStorage + Object.defineProperty(window, 'localStorage', { + value: undefined, + writable: true + }); + + expect(isLocalStorageAvailable()).toBe(false); + }); + + it('should return false when accessing localStorage throws an error', () => { + // Make accessing localStorage throw an error (as in some privacy modes) + Object.defineProperty(window, 'localStorage', { + get: () => { + throw new Error('SecurityError'); + } + }); + + expect(isLocalStorageAvailable()).toBe(false); + }); + }); +}); diff --git a/test/lib/roomApi.test.ts b/test/lib/roomApi.test.ts new file mode 100644 index 0000000..da90340 --- /dev/null +++ b/test/lib/roomApi.test.ts @@ -0,0 +1,146 @@ +import { fetchChatrooms, createRoom, deleteRoom } from '@/lib/roomApi'; + +// Mock global fetch +global.fetch = jest.fn(); + +describe('roomApi', () => { + const mockChatrooms = [ + { + id: 'room1', + roomUrl: 'abc123', + title: 'Test Room 1', + createdAt: '2023-01-01T00:00:00Z', + _count: { + messages: 10, + }, + }, + { + id: 'room2', + roomUrl: 'def456', + title: 'Test Room 2', + createdAt: '2023-01-02T00:00:00Z', + _count: { + messages: 5, + }, + }, + ]; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('fetchChatrooms', () => { + it('should fetch chatrooms successfully', async () => { + // Mock successful response + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + json: jest.fn().mockResolvedValueOnce(mockChatrooms), + }); + + const result = await fetchChatrooms(); + + expect(global.fetch).toHaveBeenCalledWith('/api/rooms'); + expect(result).toEqual(mockChatrooms); + }); + + it('should throw an error when fetch fails', async () => { + // Mock failed response + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: false, + }); + + await expect(fetchChatrooms()).rejects.toThrow('Failed to fetch chatrooms'); + expect(global.fetch).toHaveBeenCalledWith('/api/rooms'); + }); + }); + + describe('createRoom', () => { + it('should create a room successfully', async () => { + // Mock successful response + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + }); + + const title = 'New Test Room'; + await createRoom(title); + + expect(global.fetch).toHaveBeenCalledWith('/api/rooms', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ title }), + }); + }); + + it('should trim the room title before sending', async () => { + // Mock successful response + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + }); + + const title = ' New Test Room '; + await createRoom(title); + + expect(global.fetch).toHaveBeenCalledWith('/api/rooms', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ title: 'New Test Room' }), + }); + }); + + it('should throw an error when creation fails', async () => { + // Mock failed response + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: false, + }); + + await expect(createRoom('Test Room')).rejects.toThrow('Failed to create room'); + }); + }); + + describe('deleteRoom', () => { + it('should delete a room using roomUrl when available', async () => { + // Mock successful response + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + }); + + const room = mockChatrooms[0]; // Has roomUrl + await deleteRoom(room); + + expect(global.fetch).toHaveBeenCalledWith(`/api/rooms/${room.roomUrl}`, { + method: 'DELETE', + }); + }); + + it('should delete a room using id when roomUrl is not available', async () => { + // Mock successful response + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + }); + + const room = { + ...mockChatrooms[0], + roomUrl: undefined, + }; + + await deleteRoom(room); + + expect(global.fetch).toHaveBeenCalledWith(`/api/rooms/${room.id}`, { + method: 'DELETE', + }); + }); + + it('should throw an error when deletion fails', async () => { + // Mock failed response + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: false, + }); + + await expect(deleteRoom(mockChatrooms[0])).rejects.toThrow('Failed to delete chatroom'); + }); + }); +}); diff --git a/test/lib/roomUtils.test.ts b/test/lib/roomUtils.test.ts new file mode 100644 index 0000000..420ce69 --- /dev/null +++ b/test/lib/roomUtils.test.ts @@ -0,0 +1,99 @@ +import { copyRoomUrl, copyRoomId } from '@/lib/roomUtils'; +import * as clipboard from '@/lib/clipboard'; + +// Mock the clipboard module +jest.mock('../../src/lib/clipboard', () => ({ + copyToClipboard: jest.fn(), +})); + +describe('roomUtils', () => { + const mockRoom = { + id: 'room123', + roomUrl: 'abc123', + title: 'Test Room', + createdAt: '2023-01-01T00:00:00Z', + _count: { + messages: 10, + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('copyRoomUrl', () => { + it('should copy the full URL with roomUrl to clipboard', async () => { + // Mock successful clipboard copy + (clipboard.copyToClipboard as jest.Mock).mockResolvedValue(true); + + const result = await copyRoomUrl(mockRoom); + + expect(clipboard.copyToClipboard).toHaveBeenCalledWith('https://yapli.chat/abc123'); + expect(result).toBe(true); + }); + + it('should use room.id if roomUrl is not available', async () => { + // Create a room without roomUrl + const roomWithoutUrl = { + ...mockRoom, + roomUrl: undefined, + }; + + // Mock successful clipboard copy + (clipboard.copyToClipboard as jest.Mock).mockResolvedValue(true); + + const result = await copyRoomUrl(roomWithoutUrl); + + expect(clipboard.copyToClipboard).toHaveBeenCalledWith('https://yapli.chat/room123'); + expect(result).toBe(true); + }); + + it('should return false if clipboard copy fails', async () => { + // Mock failed clipboard copy + (clipboard.copyToClipboard as jest.Mock).mockResolvedValue(false); + + const result = await copyRoomUrl(mockRoom); + + expect(clipboard.copyToClipboard).toHaveBeenCalledWith('https://yapli.chat/abc123'); + expect(result).toBe(false); + }); + }); + + describe('copyRoomId', () => { + it('should copy the roomUrl to clipboard', async () => { + // Mock successful clipboard copy + (clipboard.copyToClipboard as jest.Mock).mockResolvedValue(true); + + const result = await copyRoomId(mockRoom); + + expect(clipboard.copyToClipboard).toHaveBeenCalledWith('abc123'); + expect(result).toBe(true); + }); + + it('should use room.id if roomUrl is not available', async () => { + // Create a room without roomUrl + const roomWithoutUrl = { + ...mockRoom, + roomUrl: undefined, + }; + + // Mock successful clipboard copy + (clipboard.copyToClipboard as jest.Mock).mockResolvedValue(true); + + const result = await copyRoomId(roomWithoutUrl); + + expect(clipboard.copyToClipboard).toHaveBeenCalledWith('room123'); + expect(result).toBe(true); + }); + + it('should return false if clipboard copy fails', async () => { + // Mock failed clipboard copy + (clipboard.copyToClipboard as jest.Mock).mockResolvedValue(false); + + const result = await copyRoomId(mockRoom); + + expect(clipboard.copyToClipboard).toHaveBeenCalledWith('abc123'); + expect(result).toBe(false); + }); + }); +}); From 735eedef11083a6a51810d61b0b49e42d2d1b1b6 Mon Sep 17 00:00:00 2001 From: victrixhominum Date: Wed, 9 Jul 2025 13:48:51 +0100 Subject: [PATCH 05/15] updated FormInput to use non-deprecated OnKeyDown and updated test to match --- src/components/FormInput.tsx | 6 ++++-- test/components/FormInput.test.tsx | 10 +++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/components/FormInput.tsx b/src/components/FormInput.tsx index c1e7d1d..bfcc829 100644 --- a/src/components/FormInput.tsx +++ b/src/components/FormInput.tsx @@ -10,6 +10,7 @@ interface FormInputProps { disabled?: boolean; className?: string; onKeyPress?: (e: React.KeyboardEvent) => void; + onKeyDown?: (e: React.KeyboardEvent) => void; } export default function FormInput({ @@ -24,6 +25,7 @@ export default function FormInput({ disabled = false, className = "", onKeyPress, + onKeyDown, }: FormInputProps) { return (
@@ -42,9 +44,9 @@ export default function FormInput({ placeholder={placeholder} maxLength={maxLength} disabled={disabled} - onKeyPress={onKeyPress} + onKeyDown={onKeyDown} className={`w-full px-3 py-2 border border-border rounded-md bg-card text-text focus:outline-none focus:ring-2 focus:ring-yapli-teal focus:border-transparent disabled:opacity-50 ${className}`} />
); -} \ No newline at end of file +} diff --git a/test/components/FormInput.test.tsx b/test/components/FormInput.test.tsx index f1d8e22..f74b237 100644 --- a/test/components/FormInput.test.tsx +++ b/test/components/FormInput.test.tsx @@ -67,13 +67,13 @@ describe('FormInput', () => { expect(screen.getByLabelText('Test Label')).toHaveClass('custom-class'); }); - it('calls onKeyPress when a key is pressed', () => { - const handleKeyPress = jest.fn(); - render(); + it('calls onKeyDown when a key is pressed', () => { + const handleKeyDown = jest.fn(); + render(); - fireEvent.keyPress(screen.getByLabelText('Test Label'), { key: 'Enter', code: 'Enter' }); + fireEvent.keyDown(screen.getByLabelText('Test Label'), { key: 'Enter', code: 'Enter' }); - expect(handleKeyPress).toHaveBeenCalledTimes(1); + expect(handleKeyDown).toHaveBeenCalledTimes(1); }); it('renders with different input types', () => { From 5d2d8cf76e0a550c66850c3e9783782978e27738 Mon Sep 17 00:00:00 2001 From: victrixhominum Date: Wed, 9 Jul 2025 13:54:22 +0100 Subject: [PATCH 06/15] updated alias storage to better check for local storage availabity and update the test --- src/lib/aliasStorage.ts | 20 +++++++++++++++++--- test/lib/aliasStorage.test.ts | 25 ++++++++++++++++++------- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/src/lib/aliasStorage.ts b/src/lib/aliasStorage.ts index adca84d..e7d0d85 100644 --- a/src/lib/aliasStorage.ts +++ b/src/lib/aliasStorage.ts @@ -5,7 +5,7 @@ const ALIAS_KEY_PREFIX = 'yapli_alias_'; /** - * Generate localStorage key for a specific room + * Generate a localStorage key for a specific room */ function getAliasKey(roomId: string): string { return `${ALIAS_KEY_PREFIX}${roomId}`; @@ -50,8 +50,22 @@ export function removeAlias(roomId: string): void { */ export function isLocalStorageAvailable(): boolean { try { - return typeof window !== 'undefined' && 'localStorage' in window; + // Check if a window exists (for SSR) + if (typeof window === 'undefined') { + return false; + } + + // Check if localStorage exists as a property + // This will catch both when localStorage is undefined and when accessing it throws an error + const storage = window.localStorage; + + // Additional check to ensure localStorage is actually usable + const testKey = '__test__'; + storage.setItem(testKey, testKey); + storage.removeItem(testKey); + + return true; } catch { return false; } -} \ No newline at end of file +} diff --git a/test/lib/aliasStorage.test.ts b/test/lib/aliasStorage.test.ts index 4ae81ab..802bd6c 100644 --- a/test/lib/aliasStorage.test.ts +++ b/test/lib/aliasStorage.test.ts @@ -134,14 +134,25 @@ describe('aliasStorage utility', () => { }); it('should return false when accessing localStorage throws an error', () => { - // Make accessing localStorage throw an error (as in some privacy modes) - Object.defineProperty(window, 'localStorage', { - get: () => { - throw new Error('SecurityError'); + // Save original localStorage + const originalLocalStorage = Object.getOwnPropertyDescriptor(window, 'localStorage'); + + try { + // Make accessing localStorage throw an error (as in some privacy modes) + Object.defineProperty(window, 'localStorage', { + get: () => { + throw new Error('SecurityError'); + }, + configurable: true + }); + + expect(isLocalStorageAvailable()).toBe(false); + } finally { + // Restore original localStorage after test + if (originalLocalStorage) { + Object.defineProperty(window, 'localStorage', originalLocalStorage); } - }); - - expect(isLocalStorageAvailable()).toBe(false); + } }); }); }); From 325ce7f7cf58c9bc75accb053d945c6b62d01bc8 Mon Sep 17 00:00:00 2001 From: victrixhominum Date: Wed, 9 Jul 2025 14:01:39 +0100 Subject: [PATCH 07/15] wip on component tests --- test/components/AuthProvider.test.tsx | 35 ++++++ test/components/ChatroomsList.test.tsx | 126 ++++++++++++++++++++ test/components/LinkPreview.test.tsx | 136 ++++++++++++++++++++++ test/components/MessageInput.test.tsx | 111 ++++++++++++++++++ test/components/RoomCodeButton.test.tsx | 74 ++++++++++++ test/components/RoomCreationForm.test.tsx | 90 ++++++++++++++ 6 files changed, 572 insertions(+) create mode 100644 test/components/AuthProvider.test.tsx create mode 100644 test/components/ChatroomsList.test.tsx create mode 100644 test/components/LinkPreview.test.tsx create mode 100644 test/components/MessageInput.test.tsx create mode 100644 test/components/RoomCodeButton.test.tsx create mode 100644 test/components/RoomCreationForm.test.tsx diff --git a/test/components/AuthProvider.test.tsx b/test/components/AuthProvider.test.tsx new file mode 100644 index 0000000..e3b88e7 --- /dev/null +++ b/test/components/AuthProvider.test.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import AuthProvider from '@/components/AuthProvider'; +import { SessionProvider } from 'next-auth/react'; + +// Mock next-auth/react +jest.mock('next-auth/react', () => ({ + SessionProvider: jest.fn(({ children }) =>
{children}
), +})); + +describe('AuthProvider', () => { + it('renders SessionProvider with children', () => { + const childText = 'Test Child'; + const { getByText, getByTestId } = render( + +
{childText}
+
+ ); + + expect(getByTestId('session-provider')).toBeInTheDocument(); + expect(getByText(childText)).toBeInTheDocument(); + }); + + it('passes children to SessionProvider', () => { + const { container } = render( + +
Child Component
+
+ ); + + expect(SessionProvider).toHaveBeenCalled(); + expect(container.innerHTML).toContain('Child Component'); + }); +}); diff --git a/test/components/ChatroomsList.test.tsx b/test/components/ChatroomsList.test.tsx new file mode 100644 index 0000000..2365378 --- /dev/null +++ b/test/components/ChatroomsList.test.tsx @@ -0,0 +1,126 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import ChatroomsList from '@/components/ChatroomsList'; + +// Mock the child components +jest.mock('../../src/components/RoomCodeButton', () => ({ + __esModule: true, + // @ts-expect-error for testing + default: ({ onClick, code }) => ( + + ), +})); + +jest.mock('../../src/components/CopyUrlButton', () => ({ + __esModule: true, + // @ts-expect-error for testing + default: ({ onClick, roomTitle }) => ( + + ), +})); + +jest.mock('../../src/components/DeleteRoomButton', () => ({ + __esModule: true, + // @ts-expect-error for testing + default: ({ onClick, roomTitle }) => ( + + ), +})); + +// Mock next/link +jest.mock('next/link', () => ({ + __esModule: true, + // @ts-expect-error for testing + default: ({ href, children, className }) => ( + + {children} + + ), +})); + +describe('ChatroomsList', () => { + const mockChatrooms = [ + { + id: 'room1', + roomUrl: 'test-room-1', + title: 'Test Room 1', + createdAt: '2023-01-01T00:00:00.000Z', + _count: { + messages: 10, + }, + }, + { + id: 'room2', + title: 'Test Room 2', + createdAt: '2023-01-02T00:00:00.000Z', + _count: { + messages: 5, + }, + }, + ]; + + const defaultProps = { + chatrooms: mockChatrooms, + isLoading: false, + handleCopyRoomUrl: jest.fn(), + handleCopyRoomId: jest.fn(), + handleDeleteRoom: jest.fn(), + }; + + it('renders loading state when isLoading is true', () => { + render(); + expect(screen.getByText('Loading chatrooms...')).toBeInTheDocument(); + }); + + it('renders empty state when no chatrooms are available', () => { + render(); + expect(screen.getByText("You haven't created any chatrooms yet.")).toBeInTheDocument(); + }); + + it('renders a list of chatrooms when available', () => { + render(); + expect(screen.getByText('Test Room 1')).toBeInTheDocument(); + expect(screen.getByText('Test Room 2')).toBeInTheDocument(); + expect(screen.getByText('10 messages • Created 1/1/2023')).toBeInTheDocument(); + expect(screen.getByText('5 messages • Created 1/2/2023')).toBeInTheDocument(); + }); + + it('calls handleCopyRoomUrl when copy URL button is clicked', () => { + render(); + const copyUrlButtons = screen.getAllByTestId('copy-url-button'); + fireEvent.click(copyUrlButtons[0]); + expect(defaultProps.handleCopyRoomUrl).toHaveBeenCalledWith(mockChatrooms[0]); + }); + + it('calls handleCopyRoomId when room code button is clicked', () => { + render(); + const roomCodeButtons = screen.getAllByTestId('room-code-button'); + fireEvent.click(roomCodeButtons[0]); + expect(defaultProps.handleCopyRoomId).toHaveBeenCalledWith(mockChatrooms[0]); + }); + + it('calls handleDeleteRoom when delete button is clicked', () => { + render(); + const deleteButtons = screen.getAllByTestId('delete-room-button'); + fireEvent.click(deleteButtons[0]); + expect(defaultProps.handleDeleteRoom).toHaveBeenCalledWith(mockChatrooms[0]); + }); + + it('uses roomUrl for link when available, otherwise uses id', () => { + render(); + const links = screen.getAllByTestId('next-link'); + + // First room has roomUrl + expect(links[0]).toHaveAttribute('href', '/test-room-1'); + + // The second room doesn't have roomUrl, should use id + expect(links[1]).toHaveAttribute('href', '/room2'); + }); +}); diff --git a/test/components/LinkPreview.test.tsx b/test/components/LinkPreview.test.tsx new file mode 100644 index 0000000..2697e51 --- /dev/null +++ b/test/components/LinkPreview.test.tsx @@ -0,0 +1,136 @@ +import React from 'react'; +import { render, screen, waitFor, act } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import LinkPreview from '@/components/LinkPreview'; + +// Mock next/image +jest.mock('next/image', () => ({ + __esModule: true, + default: ({ src, alt, width, height, className, onError }) => ( + {alt} + ), +})); + +// Mock fetch +global.fetch = jest.fn(); + +describe('LinkPreview', () => { + const mockUrl = 'https://example.com'; + const mockPreviewData = { + url: mockUrl, + title: 'Example Website', + description: 'This is an example website', + images: ['https://example.com/image.jpg'], + siteName: 'Example', + domain: 'example.com', + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders loading state initially', () => { + // Mock fetch to return a promise that never resolves + (global.fetch as jest.Mock).mockImplementation(() => new Promise(() => {})); + + render(); + + // Check for loading state + expect(screen.getByTestId('loading-skeleton')).toBeInTheDocument(); + }); + + it('renders preview data when fetch succeeds', async () => { + // Mock successful fetch + (global.fetch as jest.Mock).mockResolvedValue({ + ok: true, + json: jest.fn().mockResolvedValue(mockPreviewData), + }); + + render(); + + // Wait for the fetch to complete + await waitFor(() => { + expect(screen.getByText('Example Website')).toBeInTheDocument(); + }); + + expect(screen.getByText('This is an example website')).toBeInTheDocument(); + expect(screen.getByText('example.com')).toBeInTheDocument(); + expect(screen.getByTestId('next-image')).toHaveAttribute('src', 'https://example.com/image.jpg'); + expect(screen.getByRole('link')).toHaveAttribute('href', mockUrl); + }); + + it('renders nothing when fetch fails', async () => { + // Mock failed fetch + (global.fetch as jest.Mock).mockRejectedValue(new Error('Fetch failed')); + + const { container } = render(); + + // Wait for the fetch to complete + await waitFor(() => { + expect(container.firstChild).toBeNull(); + }); + }); + + it('renders nothing when response is not ok', async () => { + // Mock fetch with non-ok response + (global.fetch as jest.Mock).mockResolvedValue({ + ok: false, + status: 404, + }); + + const { container } = render(); + + // Wait for the fetch to complete + await waitFor(() => { + expect(container.firstChild).toBeNull(); + }); + }); + + it('renders with partial data when some fields are missing', async () => { + // Mock successful fetch with partial data + const partialData = { + url: mockUrl, + domain: 'example.com', + }; + + (global.fetch as jest.Mock).mockResolvedValue({ + ok: true, + json: jest.fn().mockResolvedValue(partialData), + }); + + render(); + + // Wait for the fetch to complete + await waitFor(() => { + expect(screen.getByText('example.com')).toBeInTheDocument(); + }); + + // Title and description should not be present + expect(screen.queryByRole('heading')).not.toBeInTheDocument(); + expect(screen.queryByTestId('next-image')).not.toBeInTheDocument(); + }); + + it('applies custom className when provided', async () => { + // Mock successful fetch + (global.fetch as jest.Mock).mockResolvedValue({ + ok: true, + json: jest.fn().mockResolvedValue(mockPreviewData), + }); + + render(); + + // Wait for the fetch to complete + await waitFor(() => { + const linkElement = screen.getByRole('link'); + expect(linkElement).toHaveClass('custom-class'); + }); + }); +}); diff --git a/test/components/MessageInput.test.tsx b/test/components/MessageInput.test.tsx new file mode 100644 index 0000000..697c631 --- /dev/null +++ b/test/components/MessageInput.test.tsx @@ -0,0 +1,111 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import MessageInput from '@/components/MessageInput'; + +describe('MessageInput', () => { + const defaultProps = { + onSendMessageAction: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders correctly with default props', () => { + render(); + + expect(screen.getByPlaceholderText('Type your message...')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Send' })).toBeInTheDocument(); + expect(screen.getByRole('button')).toBeDisabled(); + }); + + it('enables the send button when text is entered', () => { + render(); + + const input = screen.getByPlaceholderText('Type your message...'); + fireEvent.change(input, { target: { value: 'Hello world' } }); + + expect(screen.getByRole('button', { name: 'Send' })).not.toBeDisabled(); + }); + + it('calls onSendMessageAction when form is submitted', () => { + render(); + + const input = screen.getByPlaceholderText('Type your message...'); + fireEvent.change(input, { target: { value: 'Hello world' } }); + + const form = input.closest('form'); + fireEvent.submit(form!); + + expect(defaultProps.onSendMessageAction).toHaveBeenCalledWith('Hello world'); + expect(input).toHaveValue(''); + }); + + it('does not call onSendMessageAction when form is submitted with empty message', () => { + render(); + + const form = screen.getByPlaceholderText('Type your message...').closest('form'); + fireEvent.submit(form!); + + expect(defaultProps.onSendMessageAction).not.toHaveBeenCalled(); + }); + + it('does not call onSendMessageAction when form is submitted with only whitespace', () => { + render(); + + const input = screen.getByPlaceholderText('Type your message...'); + fireEvent.change(input, { target: { value: ' ' } }); + + const form = input.closest('form'); + fireEvent.submit(form!); + + expect(defaultProps.onSendMessageAction).not.toHaveBeenCalled(); + }); + + it('trims whitespace from message before sending', () => { + render(); + + const input = screen.getByPlaceholderText('Type your message...'); + fireEvent.change(input, { target: { value: ' Hello world ' } }); + + const form = input.closest('form'); + fireEvent.submit(form!); + + expect(defaultProps.onSendMessageAction).toHaveBeenCalledWith('Hello world'); + }); + + it('disables input when disabled prop is true', () => { + render(); + + expect(screen.getByPlaceholderText('Type your message...')).toBeDisabled(); + expect(screen.getByRole('button', { name: 'Send' })).toBeDisabled(); + }); + + it('refocuses input after sending when disabled becomes false', () => { + const { rerender } = render(); + + const input = screen.getByPlaceholderText('Type your message...'); + + // Mock focus method + const mockFocus = jest.fn(); + input.focus = mockFocus; + + // First, enable the input and add text + rerender(); + fireEvent.change(input, { target: { value: 'Hello world' } }); + + // Submit the form + const form = input.closest('form'); + fireEvent.submit(form!); + + // Disable the input + rerender(); + + // Then enable it again + rerender(); + + // Check if focus was called + expect(mockFocus).toHaveBeenCalled(); + }); +}); diff --git a/test/components/RoomCodeButton.test.tsx b/test/components/RoomCodeButton.test.tsx new file mode 100644 index 0000000..80af1ce --- /dev/null +++ b/test/components/RoomCodeButton.test.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import RoomCodeButton from '@/components/RoomCodeButton'; + +describe('RoomCodeButton', () => { + const defaultProps = { + code: 'ABC123', + onClick: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders correctly with default props', () => { + render(); + + expect(screen.getByText('ABC123')).toBeInTheDocument(); + expect(screen.getByRole('button')).toHaveAttribute('aria-label', 'Copy room code ABC123 to clipboard'); + expect(screen.getByText('Copy room code to clipboard')).toBeInTheDocument(); + }); + + it('calls onClick when button is clicked', () => { + render(); + + fireEvent.click(screen.getByRole('button')); + + expect(defaultProps.onClick).toHaveBeenCalledTimes(1); + }); + + it('renders with fullWidth class when fullWidth prop is true', () => { + render(); + + const button = screen.getByRole('button'); + expect(button).toHaveClass('w-full'); + expect(button).toHaveClass('text-center'); + }); + + it('positions tooltip correctly when fullWidth is true', () => { + render(); + + const tooltip = screen.getByText('Copy room code to clipboard'); + expect(tooltip).toHaveClass('left-4'); + expect(tooltip).not.toHaveClass('left-1/2'); + expect(tooltip).not.toHaveClass('transform'); + expect(tooltip).not.toHaveClass('-translate-x-1/2'); + }); + + it('positions tooltip correctly when fullWidth is false', () => { + render(); + + const tooltip = screen.getByText('Copy room code to clipboard'); + expect(tooltip).toHaveClass('left-1/2'); + expect(tooltip).toHaveClass('transform'); + expect(tooltip).toHaveClass('-translate-x-1/2'); + expect(tooltip).not.toHaveClass('left-4'); + }); + + it('has correct base styling regardless of fullWidth', () => { + render(); + + const button = screen.getByRole('button'); + expect(button).toHaveClass('px-2'); + expect(button).toHaveClass('py-1'); + expect(button).toHaveClass('bg-gray-300'); + expect(button).toHaveClass('text-gray-700'); + expect(button).toHaveClass('rounded'); + expect(button).toHaveClass('font-mono'); + expect(button).toHaveClass('hover:bg-gray-200'); + expect(button).toHaveClass('cursor-pointer'); + expect(button).toHaveClass('transition-colors'); + }); +}); diff --git a/test/components/RoomCreationForm.test.tsx b/test/components/RoomCreationForm.test.tsx new file mode 100644 index 0000000..5a1ab05 --- /dev/null +++ b/test/components/RoomCreationForm.test.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import RoomCreationForm from '@/components/RoomCreationForm'; + +describe('RoomCreationForm', () => { + const defaultProps = { + onSubmit: jest.fn().mockResolvedValue(undefined), + onCancel: jest.fn(), + isLoading: false, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders correctly with default props', () => { + render(); + + expect(screen.getByText('Create New Room')).toBeInTheDocument(); + expect(screen.getByLabelText('Room Title')).toBeInTheDocument(); + expect(screen.getByPlaceholderText('Enter a title for your room...')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Create Room' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument(); + }); + + it('disables submit button when room title is empty', () => { + render(); + + expect(screen.getByRole('button', { name: 'Create Room' })).toBeDisabled(); + }); + + it('enables submit button when room title is not empty', () => { + render(); + + const input = screen.getByLabelText('Room Title'); + fireEvent.change(input, { target: { value: 'Test Room' } }); + + expect(screen.getByRole('button', { name: 'Create Room' })).not.toBeDisabled(); + }); + + it('calls onSubmit with trimmed room title when form is submitted', async () => { + render(); + + const input = screen.getByLabelText('Room Title'); + fireEvent.change(input, { target: { value: ' Test Room ' } }); + + const form = input.closest('form'); + fireEvent.submit(form!); + + expect(defaultProps.onSubmit).toHaveBeenCalledWith('Test Room'); + + // Wait for the async onSubmit to complete + await waitFor(() => { + expect(input).toHaveValue(''); + }); + }); + + it('does not call onSubmit when form is submitted with empty room title', () => { + render(); + + const input = screen.getByLabelText('Room Title'); + const form = input.closest('form'); + fireEvent.submit(form!); + + expect(defaultProps.onSubmit).not.toHaveBeenCalled(); + }); + + it('calls onCancel when cancel button is clicked', () => { + render(); + + const input = screen.getByLabelText('Room Title'); + fireEvent.change(input, { target: { value: 'Test Room' } }); + + const cancelButton = screen.getByRole('button', { name: 'Cancel' }); + fireEvent.click(cancelButton); + + expect(defaultProps.onCancel).toHaveBeenCalled(); + expect(input).toHaveValue(''); + }); + + it('disables input and shows loading state when isLoading is true', () => { + render(); + + expect(screen.getByLabelText('Room Title')).toBeDisabled(); + expect(screen.getByLabelText('Room Title')).toHaveAttribute('aria-busy', 'true'); + expect(screen.getByRole('button', { name: 'Creating...' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Creating...' })).toBeDisabled(); + }); +}); From c7a4ec3a20208947a7c21bf8a3ff15e4c560b158 Mon Sep 17 00:00:00 2001 From: victrixhominum Date: Wed, 9 Jul 2025 14:09:49 +0100 Subject: [PATCH 08/15] updated to use proper template literals --- src/components/RoomCodeButton.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/RoomCodeButton.tsx b/src/components/RoomCodeButton.tsx index 5e64af0..ed6f7a4 100644 --- a/src/components/RoomCodeButton.tsx +++ b/src/components/RoomCodeButton.tsx @@ -12,7 +12,7 @@ export default function RoomCodeButton({ code, onClick, fullWidth = false }: Roo
); -} \ No newline at end of file +} From defdabc157fb8f5e2b6bd052039ddc71dd24dba0 Mon Sep 17 00:00:00 2001 From: victrixhominum Date: Wed, 9 Jul 2025 14:10:31 +0100 Subject: [PATCH 09/15] fixed the broken tests --- test/components/ChatroomsList.test.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/test/components/ChatroomsList.test.tsx b/test/components/ChatroomsList.test.tsx index 2365378..c3dda15 100644 --- a/test/components/ChatroomsList.test.tsx +++ b/test/components/ChatroomsList.test.tsx @@ -86,10 +86,10 @@ describe('ChatroomsList', () => { it('renders a list of chatrooms when available', () => { render(); - expect(screen.getByText('Test Room 1')).toBeInTheDocument(); - expect(screen.getByText('Test Room 2')).toBeInTheDocument(); - expect(screen.getByText('10 messages • Created 1/1/2023')).toBeInTheDocument(); - expect(screen.getByText('5 messages • Created 1/2/2023')).toBeInTheDocument(); + expect(screen.getAllByText('Test Room 1')[0]).toBeInTheDocument(); + expect(screen.getAllByText('Test Room 2')[0]).toBeInTheDocument(); + expect(screen.getAllByText('10 messages • Created 1/1/2023')[0]).toBeInTheDocument(); + expect(screen.getAllByText('5 messages • Created 1/2/2023')[0]).toBeInTheDocument(); }); it('calls handleCopyRoomUrl when copy URL button is clicked', () => { @@ -117,10 +117,14 @@ describe('ChatroomsList', () => { render(); const links = screen.getAllByTestId('next-link'); + // There are 4 links total (2 for each room - mobile and desktop) + // links[0] and links[1] are for the first room + // links[2] and links[3] are for the second room + // First room has roomUrl expect(links[0]).toHaveAttribute('href', '/test-room-1'); // The second room doesn't have roomUrl, should use id - expect(links[1]).toHaveAttribute('href', '/room2'); + expect(links[2]).toHaveAttribute('href', '/room2'); }); }); From 6dd5a88eb980dbcfb89f5ef761772ca83a56cfe9 Mon Sep 17 00:00:00 2001 From: victrixhominum Date: Wed, 9 Jul 2025 14:13:37 +0100 Subject: [PATCH 10/15] updated linkPreview was modified to prevent rendering a heading when only the domain is provided and grammar cleanups --- src/components/LinkPreview.tsx | 14 +++++++------- test/components/LinkPreview.test.tsx | 3 ++- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/components/LinkPreview.tsx b/src/components/LinkPreview.tsx index 04d039e..d1ad82b 100644 --- a/src/components/LinkPreview.tsx +++ b/src/components/LinkPreview.tsx @@ -28,15 +28,15 @@ export default function LinkPreview({ url, className = "" }: LinkPreviewProps) { try { setLoading(true); setError(null); - + const response = await fetch( `/api/link-preview?url=${encodeURIComponent(url)}` ); - + if (!response.ok) { throw new Error(`HTTP ${response.status}`); } - + const data = await response.json(); setPreview(data); } catch (err) { @@ -52,7 +52,7 @@ export default function LinkPreview({ url, className = "" }: LinkPreviewProps) { if (loading) { return ( -
+
@@ -66,11 +66,11 @@ export default function LinkPreview({ url, className = "" }: LinkPreviewProps) { } if (error || !preview) { - return null; // Don't show anything if preview fails + return null; // Don't show anything if the preview fails } const image = preview.images?.[0]; - const title = preview.title || preview.siteName || preview.domain; + const title = preview.title || preview.siteName; const description = preview.description; return ( @@ -113,4 +113,4 @@ export default function LinkPreview({ url, className = "" }: LinkPreviewProps) {
); -} \ No newline at end of file +} diff --git a/test/components/LinkPreview.test.tsx b/test/components/LinkPreview.test.tsx index 2697e51..8edac34 100644 --- a/test/components/LinkPreview.test.tsx +++ b/test/components/LinkPreview.test.tsx @@ -1,11 +1,12 @@ import React from 'react'; -import { render, screen, waitFor, act } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import '@testing-library/jest-dom'; import LinkPreview from '@/components/LinkPreview'; // Mock next/image jest.mock('next/image', () => ({ __esModule: true, + // @ts-expect-error for testing default: ({ src, alt, width, height, className, onError }) => ( Date: Wed, 9 Jul 2025 14:53:01 +0100 Subject: [PATCH 11/15] added missing tests for lib files --- test/lib/auth.test.ts | 434 ++++++++++++++++++++++++++++++++++++++++ test/lib/prisma.test.ts | 84 ++++++++ test/lib/socket.test.ts | 311 ++++++++++++++++++++++++++++ 3 files changed, 829 insertions(+) create mode 100644 test/lib/auth.test.ts create mode 100644 test/lib/prisma.test.ts create mode 100644 test/lib/socket.test.ts diff --git a/test/lib/auth.test.ts b/test/lib/auth.test.ts new file mode 100644 index 0000000..f7f7d8d --- /dev/null +++ b/test/lib/auth.test.ts @@ -0,0 +1,434 @@ +// Mock environment variables +const originalEnv = process.env; + +// Create mock functions for testing +const mockFindUnique = jest.fn(); +const mockCreate = jest.fn(); +const mockCompare = jest.fn(); + +// Mock the modules +jest.mock('../../src/lib/prisma', () => ({ + prisma: { + user: { + findUnique: mockFindUnique, + create: mockCreate, + }, + }, +})); + +jest.mock('bcryptjs', () => ({ + compare: mockCompare, +})); + +// Create a function to get fresh authOptions with current environment variables +const getAuthOptions = () => { + // Clear module cache to ensure fresh import with current env vars + jest.resetModules(); + + // Reset mocks before each test + mockFindUnique.mockReset(); + mockCreate.mockReset(); + mockCompare.mockReset(); + + // Create a custom implementation of the auth options + return { + providers: [ + { + id: 'credentials', + name: 'credentials', + credentials: { + email: { label: "Email", type: "email" }, + password: { label: "Password", type: "password" }, + }, + authorize: async (credentials: { email: string; password: string; }) => { + if (!credentials?.email || !credentials?.password) { + return null; + } + + const user = await mockFindUnique({ + where: { email: credentials.email }, + }); + + if (!user || !user.password) { + return null; + } + + const isPasswordValid = await mockCompare( + credentials.password, + user.password + ); + + if (!isPasswordValid) { + return null; + } + + return { + id: user.id, + email: user.email, + name: user.name, + image: user.image, + }; + }, + }, + { + id: 'google', + name: 'Google', + }, + { + id: 'github', + name: 'GitHub', + }, + ], + callbacks: { + // @ts-expect-error easier to not type for testing + signIn: async ({ user, account }) => { + if (account?.provider === "credentials") { + return true; + } + + if (account?.provider && user.email) { + try { + const existingUser = await mockFindUnique({ + where: { email: user.email }, + }); + + if (!existingUser) { + await mockCreate({ + data: { + email: user.email, + name: user.name, + image: user.image, + }, + }); + } + return true; + } catch (error) { + console.error("Error creating user:", error); + return false; + } + } + + return true; + }, + // @ts-expect-error easier to not type for testing + jwt: async ({ token, user, account }) => { + if (user) { + if (account?.provider === "credentials") { + token.sub = user.id; + } else { + const dbUser = await mockFindUnique({ + where: { email: user.email }, + }); + token.sub = dbUser?.id; + } + } + return token; + }, + // @ts-expect-error easier to not type for testing + session: ({ session, token }) => ({ + ...session, + user: { + ...session.user, + id: token.sub, + }, + }), + }, + session: { + strategy: "jwt", + }, + pages: { + signIn: "/auth/signin", + }, + }; +}; + +describe('auth configuration', () => { + beforeEach(() => { + jest.resetAllMocks(); + process.env = { ...originalEnv }; + process.env.GOOGLE_CLIENT_ID = 'google-client-id'; + process.env.GOOGLE_CLIENT_SECRET = 'google-client-secret'; + process.env.GITHUB_ID = 'github-id'; + process.env.GITHUB_SECRET = 'github-secret'; + }); + + afterAll(() => { + process.env = originalEnv; + }); + + describe('providers', () => { + it('should have credentials, Google, and GitHub providers', () => { + const options = getAuthOptions(); + expect(options.providers).toHaveLength(3); + + const providerTypes = options.providers.map(provider => provider.id); + expect(providerTypes).toContain('credentials'); + expect(providerTypes).toContain('google'); + expect(providerTypes).toContain('github'); + }); + + it('should configure Google provider with environment variables', () => { + const options = getAuthOptions(); + const googleProvider = options.providers.find(p => p.id === 'google'); + expect(googleProvider).toBeDefined(); + }); + + it('should configure GitHub provider with environment variables', () => { + const options = getAuthOptions(); + const githubProvider = options.providers.find(p => p.id === 'github'); + expect(githubProvider).toBeDefined(); + }); + }); + + describe('credentials provider authorize function', () => { + + it('should return null if user is not found', async () => { + const options = getAuthOptions(); + const credentialsProvider = options.providers.find(p => p.id === 'credentials'); + const authorizeFunction = credentialsProvider?.authorize; + + mockFindUnique.mockResolvedValue(null); + + const result = await authorizeFunction?.({ + email: 'test@example.com', + password: 'password123' + }); + + expect(mockFindUnique).toHaveBeenCalledWith({ + where: { email: 'test@example.com' } + }); + expect(result).toBeNull(); + }); + + it('should return null if user has no password (OAuth user)', async () => { + const options = getAuthOptions(); + const credentialsProvider = options.providers.find(p => p.id === 'credentials'); + const authorizeFunction = credentialsProvider?.authorize; + + mockFindUnique.mockResolvedValue({ + id: 'user-id', + email: 'test@example.com', + name: 'Test User', + password: null + }); + + const result = await authorizeFunction?.({ + email: 'test@example.com', + password: 'password123' + }); + + expect(result).toBeNull(); + }); + + it('should return null if password is invalid', async () => { + const options = getAuthOptions(); + const credentialsProvider = options.providers.find(p => p.id === 'credentials'); + const authorizeFunction = credentialsProvider?.authorize; + + mockFindUnique.mockResolvedValue({ + id: 'user-id', + email: 'test@example.com', + name: 'Test User', + password: 'hashed-password' + }); + mockCompare.mockResolvedValue(false); + + const result = await authorizeFunction?.({ + email: 'test@example.com', + password: 'wrong-password' + }); + + expect(mockCompare).toHaveBeenCalledWith('wrong-password', 'hashed-password'); + expect(result).toBeNull(); + }); + + it('should return user data if credentials are valid', async () => { + const options = getAuthOptions(); + const credentialsProvider = options.providers.find(p => p.id === 'credentials'); + const authorizeFunction = credentialsProvider?.authorize; + + const mockUser = { + id: 'user-id', + email: 'test@example.com', + name: 'Test User', + image: 'https://example.com/avatar.jpg', + password: 'hashed-password' + }; + + mockFindUnique.mockResolvedValue(mockUser); + mockCompare.mockResolvedValue(true); + + const result = await authorizeFunction?.({ + email: 'test@example.com', + password: 'correct-password' + }); + + expect(result).toEqual({ + id: 'user-id', + email: 'test@example.com', + name: 'Test User', + image: 'https://example.com/avatar.jpg' + }); + }); + }); + + describe('callbacks', () => { + describe('signIn callback', () => { + it('should return true for credentials provider', async () => { + const options = getAuthOptions(); + const result = await options.callbacks.signIn({ + user: { id: 'user-id', email: 'test@example.com' }, + account: { provider: 'credentials' }, + }); + + expect(result).toBe(true); + }); + + it('should create a new user for OAuth provider if user does not exist', async () => { + const options = getAuthOptions(); + mockFindUnique.mockResolvedValue(null); + mockCreate.mockResolvedValue({ + id: 'new-user-id', + email: 'oauth@example.com', + name: 'OAuth User' + }); + + const result = await options.callbacks.signIn({ + user: { + email: 'oauth@example.com', + name: 'OAuth User', + image: 'https://example.com/avatar.jpg' + }, + account: { provider: 'google' }, + }); + + expect(mockFindUnique).toHaveBeenCalledWith({ + where: { email: 'oauth@example.com' } + }); + expect(mockCreate).toHaveBeenCalledWith({ + data: { + email: 'oauth@example.com', + name: 'OAuth User', + image: 'https://example.com/avatar.jpg' + } + }); + expect(result).toBe(true); + }); + + it('should not create a user if one already exists with the email', async () => { + const options = getAuthOptions(); + mockFindUnique.mockResolvedValue({ + id: 'existing-user-id', + email: 'oauth@example.com', + name: 'Existing User' + }); + + const result = await options.callbacks.signIn({ + user: { + email: 'oauth@example.com', + name: 'OAuth User' + }, + account: { provider: 'google' }, + }); + + expect(mockFindUnique).toHaveBeenCalledWith({ + where: { email: 'oauth@example.com' } + }); + expect(mockCreate).not.toHaveBeenCalled(); + expect(result).toBe(true); + }); + + it('should return false if there is an error creating the user', async () => { + const options = getAuthOptions(); + mockFindUnique.mockResolvedValue(null); + mockCreate.mockRejectedValue(new Error('Database error')); + + // Mock console.error to prevent test output noise + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); + + const result = await options.callbacks.signIn({ + user: { + email: 'oauth@example.com', + name: 'OAuth User' + }, + account: { provider: 'google' }, + }); + + expect(consoleErrorSpy).toHaveBeenCalled(); + expect(result).toBe(false); + + consoleErrorSpy.mockRestore(); + }); + }); + + describe('jwt callback', () => { + it('should set token.sub to user.id for credentials provider', async () => { + const options = getAuthOptions(); + const token = { sub: undefined }; + const user = { id: 'user-id' }; + const account = { provider: 'credentials' }; + + const result = await options.callbacks.jwt({ token, user, account }); + + expect(result.sub).toBe('user-id'); + }); + + it('should set token.sub to database user id for OAuth providers', async () => { + const options = getAuthOptions(); + const token = { sub: undefined }; + const user = { email: 'oauth@example.com' }; + const account = { provider: 'google' }; + + mockFindUnique.mockResolvedValue({ + id: 'db-user-id', + email: 'oauth@example.com' + }); + + const result = await options.callbacks.jwt({ token, user, account }); + + expect(mockFindUnique).toHaveBeenCalledWith({ + where: { email: 'oauth@example.com' } + }); + expect(result.sub).toBe('db-user-id'); + }); + + it('should return token unchanged if no user is provided', async () => { + const options = getAuthOptions(); + const token = { sub: 'existing-sub' }; + // @ts-expect-error needed for test + const result = await options.callbacks.jwt({ token }); + + expect(result).toBe(token); + }); + }); + + describe('session callback', () => { + it('should add user.id from token.sub to session', () => { + const options = getAuthOptions(); + const session = { user: { name: 'Test User' } }; + const token = { sub: 'user-id' }; + + const result = options.callbacks.session({ session, token }); + + expect(result).toEqual({ + user: { + name: 'Test User', + id: 'user-id' + } + }); + }); + }); + }); + + describe('configuration options', () => { + it('should use jwt session strategy', () => { + const options = getAuthOptions(); + expect(options.session.strategy).toBe('jwt'); + }); + + it('should set custom sign in page', () => { + const options = getAuthOptions(); + expect(options.pages.signIn).toBe('/auth/signin'); + }); + }); +}); diff --git a/test/lib/prisma.test.ts b/test/lib/prisma.test.ts new file mode 100644 index 0000000..2ee415c --- /dev/null +++ b/test/lib/prisma.test.ts @@ -0,0 +1,84 @@ +/* + * During this file you will find many instances of + * // eslint-disable-next-line @typescript-eslint/no-require-imports + * This is not good practice, but due to the nature of prisma imports its the + * best way to stub the methods for testing. + */ + +// Mock PrismaClient +jest.mock('@prisma/client', () => { + const mockPrismaClient = jest.fn().mockImplementation(() => ({ + // Mock any methods used in the prisma.ts file + // In this case; we don't need to mock any specific methods + })); + return { PrismaClient: mockPrismaClient }; +}); + +describe('prisma client', () => { + let originalNodeEnv: string | undefined; + + beforeEach(() => { + originalNodeEnv = process.env.NODE_ENV; + jest.resetModules(); + }); + + afterEach(() => { + // @ts-expect-error deliberately overwriting + process.env.NODE_ENV = originalNodeEnv; + // Clean up the global object + // eslint-disable-next-line + if ((globalThis as any).prisma) { + // eslint-disable-next-line + delete (globalThis as any).prisma; + } + }); + + it('should create a new PrismaClient instance', () => { + // Import modules inside the test to ensure they're affected by the environment setup + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { PrismaClient } = require('@prisma/client'); + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { prisma } = require("../../src/lib/prisma"); + + expect(PrismaClient).toHaveBeenCalledTimes(1); + expect(PrismaClient).toHaveBeenCalledWith({ + log: [], + }); + expect(prisma).toBeDefined(); + }); + + it('should reuse the existing PrismaClient instance on subsequent imports', () => { + // Import modules inside the test to ensure they're affected by the environment setup + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { PrismaClient } = require('@prisma/client'); + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { prisma: prisma1 } = require("../../src/lib/prisma"); + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { prisma: prisma2 } = require("../../src/lib/prisma"); + + expect(PrismaClient).toHaveBeenCalledTimes(1); + expect(prisma1).toBe(prisma2); + }); + + it('should store the PrismaClient instance in the global object in non-production environments', () => { + // @ts-expect-error deliberately overwriting + process.env.NODE_ENV = 'development'; + + // Import modules inside the test to ensure they're affected by the environment setup + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { prisma } = require("../../src/lib/prisma"); + // eslint-disable-next-line + expect((globalThis as any).prisma).toBe(prisma); + }); + + it('should not store the PrismaClient instance in the global object in production', () => { + // @ts-expect-error deliberately overwriting + process.env.NODE_ENV = 'production'; + + // Import modules inside the test to ensure they're affected by the environment setup + // eslint-disable-next-line @typescript-eslint/no-require-imports + require("../../src/lib/prisma"); + // eslint-disable-next-line + expect((globalThis as any).prisma).toBeUndefined(); + }); +}); diff --git a/test/lib/socket.test.ts b/test/lib/socket.test.ts new file mode 100644 index 0000000..d358e98 --- /dev/null +++ b/test/lib/socket.test.ts @@ -0,0 +1,311 @@ +import { renderHook, act } from '@testing-library/react'; +import { useSocket } from '@/lib/socket'; +import { io } from 'socket.io-client'; + +// Mock socket.io-client +jest.mock('socket.io-client', () => { + const mockSocket = { + on: jest.fn(), + emit: jest.fn(), + disconnect: jest.fn(), + }; + return { + io: jest.fn(() => mockSocket), + }; +}); + +describe('useSocket hook', () => { + const mockRoomId = 'room-123'; + const mockOnNewMessage = jest.fn(); + const mockOnUsersUpdated = jest.fn(); + const mockOnAliasRejected = jest.fn(); + // any isn't allowed, but mocks are hard + // eslint-disable-next-line + let mockSocket: any; + + beforeEach(() => { + jest.clearAllMocks(); + mockSocket = (io as jest.Mock)(); + // Reset the mock implementation for on to avoid affecting other tests + mockSocket.on.mockImplementation(() => {}); + }); + + it('should initialize socket connection and join room', () => { + renderHook(() => + useSocket({ + roomId: mockRoomId, + onNewMessage: mockOnNewMessage, + onUsersUpdated: mockOnUsersUpdated, + onAliasRejected: mockOnAliasRejected, + }) + ); + + // Check if socket.io client was initialized + expect(io).toHaveBeenCalled(); + + // Check event listeners were set up + expect(mockSocket.on).toHaveBeenCalledWith('connect', expect.any(Function)); + expect(mockSocket.on).toHaveBeenCalledWith('disconnect', expect.any(Function)); + expect(mockSocket.on).toHaveBeenCalledWith('new-message', expect.any(Function)); + expect(mockSocket.on).toHaveBeenCalledWith('users-updated', expect.any(Function)); + expect(mockSocket.on).toHaveBeenCalledWith('alias-rejected', expect.any(Function)); + }); + + it('should emit join-room event when connected', () => { + renderHook(() => + useSocket({ + roomId: mockRoomId, + onNewMessage: mockOnNewMessage, + onUsersUpdated: mockOnUsersUpdated, + onAliasRejected: mockOnAliasRejected, + }) + ); + + // Get the connect callback + const connectCallback = mockSocket.on.mock.calls.find( + (call: string[]) => call[0] === 'connect' + )[1]; + + // Call the connect callback + connectCallback(); + + // Check if join-room was emitted with correct roomId + expect(mockSocket.emit).toHaveBeenCalledWith('join-room', mockRoomId); + }); + + it('should call onNewMessage when new-message event is received for the correct room', () => { + renderHook(() => + useSocket({ + roomId: mockRoomId, + onNewMessage: mockOnNewMessage, + onUsersUpdated: mockOnUsersUpdated, + onAliasRejected: mockOnAliasRejected, + }) + ); + + // Get the new-message callback + const newMessageCallback = mockSocket.on.mock.calls.find( + (call: string[]) => call[0] === 'new-message' + )[1]; + + const mockMessage = { + id: 'msg-1', + chatroomId: mockRoomId, + alias: 'User1', + message: 'Hello world', + timestamp: '2023-01-01T12:00:00Z', + }; + + // Call the new-message callback + newMessageCallback(mockMessage); + + // Check if onNewMessage was called with the message + expect(mockOnNewMessage).toHaveBeenCalledWith(mockMessage); + }); + + it('should not call onNewMessage when new-message event is for a different room', () => { + renderHook(() => + useSocket({ + roomId: mockRoomId, + onNewMessage: mockOnNewMessage, + onUsersUpdated: mockOnUsersUpdated, + onAliasRejected: mockOnAliasRejected, + }) + ); + + // Get the new-message callback + const newMessageCallback = mockSocket.on.mock.calls.find( + (call: string[]) => call[0] === 'new-message' + )[1]; + + const mockMessage = { + id: 'msg-1', + chatroomId: 'different-room', + alias: 'User1', + message: 'Hello world', + timestamp: '2023-01-01T12:00:00Z', + }; + + // Call the new-message callback + newMessageCallback(mockMessage); + + // Check that onNewMessage was not called + expect(mockOnNewMessage).not.toHaveBeenCalled(); + }); + + it('should call onUsersUpdated when users-updated event is received', () => { + renderHook(() => + useSocket({ + roomId: mockRoomId, + onNewMessage: mockOnNewMessage, + onUsersUpdated: mockOnUsersUpdated, + onAliasRejected: mockOnAliasRejected, + }) + ); + + // Get the users-updated callback + const usersUpdatedCallback = mockSocket.on.mock.calls.find( + (call: string[]) => call[0] === 'users-updated' + )[1]; + + const mockUsers = ['User1', 'User2', 'User3']; + + // Call the users-updated callback + usersUpdatedCallback(mockUsers); + + // Check if onUsersUpdated was called with the users array + expect(mockOnUsersUpdated).toHaveBeenCalledWith(mockUsers); + }); + + it('should call onAliasRejected when alias-rejected event is received', () => { + renderHook(() => + useSocket({ + roomId: mockRoomId, + onNewMessage: mockOnNewMessage, + onUsersUpdated: mockOnUsersUpdated, + onAliasRejected: mockOnAliasRejected, + }) + ); + + // Get the alias-rejected callback + const aliasRejectedCallback = mockSocket.on.mock.calls.find( + (call: string[]) => call[0] === 'alias-rejected' + )[1]; + + const mockReason = 'Alias already taken'; + + // Call the alias-rejected callback + aliasRejectedCallback({ reason: mockReason }); + + // Check if onAliasRejected was called with the reason + expect(mockOnAliasRejected).toHaveBeenCalledWith(mockReason); + }); + + it('should disconnect socket when component unmounts', () => { + const { unmount } = renderHook(() => + useSocket({ + roomId: mockRoomId, + onNewMessage: mockOnNewMessage, + onUsersUpdated: mockOnUsersUpdated, + onAliasRejected: mockOnAliasRejected, + }) + ); + + // Unmount the component + unmount(); + + // Check if disconnect was called + expect(mockSocket.disconnect).toHaveBeenCalled(); + }); + + it('should emit set-alias event when setAlias is called', () => { + // Mock isConnected state + mockSocket.on.mockImplementation((event: string, callback: () => void) => { + if (event === 'connect') { + callback(); // This will set isConnected to true + } + }); + + const { result } = renderHook(() => + useSocket({ + roomId: mockRoomId, + onNewMessage: mockOnNewMessage, + onUsersUpdated: mockOnUsersUpdated, + onAliasRejected: mockOnAliasRejected, + }) + ); + + // Call setAlias + act(() => { + result.current.setAlias('TestUser'); + }); + + // Check if set-alias was emitted with correct alias + expect(mockSocket.emit).toHaveBeenCalledWith('set-alias', 'TestUser'); + }); + + it('should not emit set-alias event when not connected', () => { + const { result } = renderHook(() => + useSocket({ + roomId: mockRoomId, + onNewMessage: mockOnNewMessage, + onUsersUpdated: mockOnUsersUpdated, + onAliasRejected: mockOnAliasRejected, + }) + ); + + // Reset mock to clear any previous calls + mockSocket.emit.mockClear(); + + // Call setAlias (without triggering connect callback) + act(() => { + result.current.setAlias('TestUser'); + }); + + // Check that set-alias was not emitted + expect(mockSocket.emit).not.toHaveBeenCalledWith('set-alias', 'TestUser'); + }); + + it('should emit send-message event when emitMessage is called', () => { + const { result } = renderHook(() => + useSocket({ + roomId: mockRoomId, + onNewMessage: mockOnNewMessage, + onUsersUpdated: mockOnUsersUpdated, + onAliasRejected: mockOnAliasRejected, + }) + ); + + // Call emitMessage + act(() => { + result.current.emitMessage('TestUser', 'Hello world'); + }); + + // Check if a send message was emitted with correct data + expect(mockSocket.emit).toHaveBeenCalledWith('send-message', { + roomId: mockRoomId, + alias: 'TestUser', + message: 'Hello world', + }); + }); + + it('should update isConnected state on connect and disconnect events', () => { + const { result } = renderHook(() => + useSocket({ + roomId: mockRoomId, + onNewMessage: mockOnNewMessage, + onUsersUpdated: mockOnUsersUpdated, + onAliasRejected: mockOnAliasRejected, + }) + ); + + // Initially isConnected should be false + expect(result.current.isConnected).toBe(false); + + // Get the connect callback + const connectCallback = mockSocket.on.mock.calls.find( + (call: string[]) => call[0] === 'connect' + )[1]; + + // Call the connect callback + act(() => { + connectCallback(); + }); + + // Now isConnected should be true + expect(result.current.isConnected).toBe(true); + + // Get the disconnect callback + const disconnectCallback = mockSocket.on.mock.calls.find( + (call: string[]) => call[0] === 'disconnect' + )[1]; + + // Call the disconnect callback + act(() => { + disconnectCallback(); + }); + + // Now isConnected should be false again + expect(result.current.isConnected).toBe(false); + }); +}); From cad99ba98870e9ec76e74e7b5e1081f1594b252b Mon Sep 17 00:00:00 2001 From: victrixhominum Date: Wed, 9 Jul 2025 15:34:22 +0100 Subject: [PATCH 12/15] fixed issue with datetime --- test/components/ChatroomsList.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/components/ChatroomsList.test.tsx b/test/components/ChatroomsList.test.tsx index c3dda15..6211f58 100644 --- a/test/components/ChatroomsList.test.tsx +++ b/test/components/ChatroomsList.test.tsx @@ -88,8 +88,8 @@ describe('ChatroomsList', () => { render(); expect(screen.getAllByText('Test Room 1')[0]).toBeInTheDocument(); expect(screen.getAllByText('Test Room 2')[0]).toBeInTheDocument(); - expect(screen.getAllByText('10 messages • Created 1/1/2023')[0]).toBeInTheDocument(); - expect(screen.getAllByText('5 messages • Created 1/2/2023')[0]).toBeInTheDocument(); + expect(screen.getAllByText(/10 messages • Created/)[0]).toBeInTheDocument(); + expect(screen.getAllByText(/5 messages • Created/)[0]).toBeInTheDocument(); }); it('calls handleCopyRoomUrl when copy URL button is clicked', () => { From 8bf7ad9b8c9c7faaf3c07a8800d0c8deccac93d6 Mon Sep 17 00:00:00 2001 From: victrixhominum Date: Wed, 9 Jul 2025 15:41:18 +0100 Subject: [PATCH 13/15] added husky precommit hook --- .husky/_/husky.sh | 32 + .husky/pre-commit | 8 + package-lock.json | 1439 +++++++++++++++++++++++++++++++++++++++++++-- package.json | 11 +- 4 files changed, 1455 insertions(+), 35 deletions(-) create mode 100755 .husky/_/husky.sh create mode 100755 .husky/pre-commit diff --git a/.husky/_/husky.sh b/.husky/_/husky.sh new file mode 100755 index 0000000..a09c6ca --- /dev/null +++ b/.husky/_/husky.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env sh +if [ -z "$husky_skip_init" ]; then + debug () { + if [ "$HUSKY_DEBUG" = "1" ]; then + echo "husky (debug) - $1" + fi + } + + readonly hook_name="$(basename -- "$0")" + debug "starting $hook_name..." + + if [ "$HUSKY" = "0" ]; then + debug "HUSKY env variable is set to 0, skipping hook" + exit 0 + fi + + if [ -f ~/.huskyrc ]; then + debug "sourcing ~/.huskyrc" + . ~/.huskyrc + fi + + readonly husky_skip_init=1 + export husky_skip_init + sh -e "$0" "$@" + exitCode="$?" + + if [ $exitCode != 0 ]; then + echo "husky - $hook_name hook exited with code $exitCode (error)" + fi + + exit $exitCode +fi diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..b1519b6 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,8 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +# Run lint-staged to lint staged files +npx lint-staged + +# Run tests +npm test diff --git a/package-lock.json b/package-lock.json index 0da2244..cc13e34 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,8 +37,11 @@ "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "15.3.2", + "husky": "^8.0.3", "jest": "^30.0.4", "jest-environment-jsdom": "^30.0.4", + "lint-staged": "^15.2.0", + "nyc": "^15.1.0", "prisma": "^6.8.2", "tailwind-merge": "^3.3.0", "tailwindcss": "^4", @@ -4186,6 +4189,19 @@ "node": ">= 14" } }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -4259,6 +4275,24 @@ "node": ">= 8" } }, + "node_modules/append-transform": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", + "dev": true, + "dependencies": { + "default-require-extensions": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", + "dev": true + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -4699,6 +4733,63 @@ "node": ">=10.16.0" } }, + "node_modules/caching-transform": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", + "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", + "dev": true, + "dependencies": { + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/caching-transform/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caching-transform/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/caching-transform/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/caching-transform/node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -4883,6 +4974,69 @@ "integrity": "sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==", "dev": true }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -5031,6 +5185,27 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -5233,6 +5408,15 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/decimal.js": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", @@ -5269,6 +5453,30 @@ "node": ">=0.10.0" } }, + "node_modules/default-require-extensions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", + "integrity": "sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==", + "dev": true, + "dependencies": { + "strip-bom": "^4.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-require-extensions/node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -5578,6 +5786,18 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -5767,6 +5987,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, "node_modules/esbuild": { "version": "0.25.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", @@ -6269,6 +6495,12 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -6420,6 +6652,47 @@ "node": ">=8" } }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-cache-dir/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-cache-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -6490,6 +6763,26 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/fromentries": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", + "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -6570,6 +6863,18 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -6856,6 +7161,31 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hasha": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", + "dev": true, + "dependencies": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hasha/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -6941,6 +7271,21 @@ "node": ">=10.17.0" } }, + "node_modules/husky": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz", + "integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==", + "dev": true, + "bin": { + "husky": "lib/bin.js" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -7435,6 +7780,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true + }, "node_modules/is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -7481,6 +7832,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -7504,6 +7864,18 @@ "node": ">=8" } }, + "node_modules/istanbul-lib-hook": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", + "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", + "dev": true, + "dependencies": { + "append-transform": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/istanbul-lib-instrument": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", @@ -7520,16 +7892,33 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "node_modules/istanbul-lib-processinfo": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz", + "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==", "dev": true, "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, + "archy": "^1.0.0", + "cross-spawn": "^7.0.3", + "istanbul-lib-coverage": "^3.2.0", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, "engines": { "node": ">=10" } @@ -8939,6 +9328,18 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -8974,6 +9375,236 @@ "integrity": "sha512-DRSlB9DKVW04c4SUdGvKK5FR6be45lTU9M76JnngqPeeGDqPwYc0zdUErtsNVMtxPXgUWV4HbXbnC4sNyBxkYg==", "license": "MIT" }, + "node_modules/lint-staged": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.5.2.tgz", + "integrity": "sha512-YUSOLq9VeRNAo/CTaVmhGDKG+LBtA8KF1X4K5+ykMSwWST1vDxJRB2kv2COgLb1fvpCo+A/y9A0G0znNVmdx4w==", + "dev": true, + "dependencies": { + "chalk": "^5.4.1", + "commander": "^13.1.0", + "debug": "^4.4.0", + "execa": "^8.0.1", + "lilconfig": "^3.1.3", + "listr2": "^8.2.5", + "micromatch": "^4.0.8", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.7.0" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/lint-staged/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/lint-staged/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/lint-staged/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/lint-staged/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/lint-staged/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/lint-staged/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/lint-staged/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/lint-staged/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/lint-staged/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/listr2": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.3.3.tgz", + "integrity": "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==", + "dev": true, + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true + }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -8996,6 +9627,12 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, + "node_modules/lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -9003,6 +9640,123 @@ "dev": true, "license": "MIT" }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "dev": true, + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "dev": true, + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -9154,6 +9908,18 @@ "node": ">=6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -9410,51 +10176,371 @@ "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "dev": true }, - "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "node_modules/node-preload": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", + "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", + "dev": true, + "dependencies": { + "process-on-spawn": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nwsapi": { + "version": "2.2.20", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", + "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", + "dev": true + }, + "node_modules/nyc": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", + "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==", + "dev": true, + "dependencies": { + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "caching-transform": "^4.0.0", + "convert-source-map": "^1.7.0", + "decamelize": "^1.2.0", + "find-cache-dir": "^3.2.0", + "find-up": "^4.1.0", + "foreground-child": "^2.0.0", + "get-package-type": "^0.1.0", + "glob": "^7.1.6", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-hook": "^3.0.0", + "istanbul-lib-instrument": "^4.0.0", + "istanbul-lib-processinfo": "^2.0.2", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "make-dir": "^3.0.0", + "node-preload": "^0.2.1", + "p-map": "^3.0.0", + "process-on-spawn": "^1.0.0", + "resolve-from": "^5.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "spawn-wrap": "^2.0.0", + "test-exclude": "^6.0.0", + "yargs": "^15.0.2" + }, + "bin": { + "nyc": "bin/nyc.js" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/nyc/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/nyc/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/nyc/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/nyc/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/nyc/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/nyc/node_modules/istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/nyc/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nyc/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nyc/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/nyc/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "node_modules/nyc/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "node_modules/nyc/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "dependencies": { - "path-key": "^3.0.0" + "ansi-regex": "^5.0.1" }, "engines": { "node": ">=8" } }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "license": "BSD-2-Clause", + "node_modules/nyc/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, "dependencies": { - "boolbase": "^1.0.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" + "engines": { + "node": ">=8" } }, - "node_modules/nwsapi": { - "version": "2.2.20", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", - "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", + "node_modules/nyc/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", "dev": true }, + "node_modules/nyc/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/oauth": { "version": "0.9.15", "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", @@ -9719,6 +10805,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -9728,6 +10826,21 @@ "node": ">=6" } }, + "node_modules/package-hash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", + "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.15", + "hasha": "^5.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -9879,6 +10992,18 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/pirates": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", @@ -10055,6 +11180,18 @@ } } }, + "node_modules/process-on-spawn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.1.0.tgz", + "integrity": "sha512-JOnOPQ/8TZgjs1JIH/m9ni7FfimjNa/PRx7y/Wb5qdItsnhO0jE4AT7fC0HjC28DUQWDr50dwSYZLdRMlqDq3Q==", + "dev": true, + "dependencies": { + "fromentries": "^1.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -10208,6 +11345,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==", + "dev": true, + "dependencies": { + "es6-error": "^4.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -10217,6 +11366,12 @@ "node": ">=0.10.0" } }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -10279,6 +11434,37 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -10290,6 +11476,49 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/rrweb-cssom": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", @@ -10412,6 +11641,12 @@ "node": ">=10" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -10633,6 +11868,46 @@ "node": ">=8" } }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/socket.io": { "version": "4.8.1", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", @@ -10785,6 +12060,66 @@ "source-map": "^0.6.0" } }, + "node_modules/spawn-wrap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", + "dev": true, + "dependencies": { + "foreground-child": "^2.0.0", + "is-windows": "^1.0.2", + "make-dir": "^3.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "which": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/spawn-wrap/node_modules/foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/spawn-wrap/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/spawn-wrap/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/spawn-wrap/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -10827,6 +12162,15 @@ "node": ">=10.0.0" } }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -11578,6 +12922,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", @@ -11892,6 +13245,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "dev": true + }, "node_modules/which-typed-array": { "version": "1.1.19", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", @@ -12094,6 +13453,18 @@ "node": ">=18" } }, + "node_modules/yaml": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/package.json b/package.json index 684a790..33318ef 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,13 @@ "start": "node -r dotenv/config server.js dotenv_config_path=.env.production", "lint": "next lint", "test": "jest", - "test:watch": "jest --watch" + "test:watch": "jest --watch", + "prepare": "husky" + }, + "lint-staged": { + "*.{js,jsx,ts,tsx}": [ + "eslint --fix" + ] }, "dependencies": { "@auth/prisma-adapter": "^2.9.1", @@ -41,8 +47,11 @@ "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "15.3.2", + "husky": "^8.0.3", "jest": "^30.0.4", "jest-environment-jsdom": "^30.0.4", + "lint-staged": "^15.2.0", + "nyc": "^15.1.0", "prisma": "^6.8.2", "tailwind-merge": "^3.3.0", "tailwindcss": "^4", From f65b0cc07589940bcc929947123f10349650fbca Mon Sep 17 00:00:00 2001 From: victrixhominum Date: Wed, 9 Jul 2025 15:43:42 +0100 Subject: [PATCH 14/15] added docs on husky precommit --- docs/CONTRIBUTING.md | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index d025678..dd3f343 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -29,32 +29,52 @@ cd yapli 2. Set up environment variables into `.env.local` (see `.env.example`) 3. Start development server: `npm run dev` -### 4. Create a Branch +### 4. Pre-commit Hooks with Husky + +Yapli uses [Husky](https://typicode.github.io/husky/) to run pre-commit hooks that ensure code quality before commits: + +1. **Automatic Setup**: Husky is automatically installed and configured when you run `npm install` (via the `prepare` script) +2. **Pre-commit Hook**: The pre-commit hook runs: + - `lint-staged`: Runs ESLint on staged JavaScript/TypeScript files + - `npm test`: Runs all tests to ensure nothing breaks + +If you're not seeing the pre-commit hooks run when you commit: + +1. Ensure Git hooks are enabled: `git config core.hooksPath .husky` +2. Make sure Husky is installed: `npm run prepare` +3. Verify the pre-commit hook is executable: `chmod +x .husky/pre-commit` + +To temporarily bypass the pre-commit hook (not recommended): +```bash +git commit -m "Your message" --no-verify +``` + +### 5. Create a Branch ```bash git checkout -b your-branch-name ``` -### 5. Make Your Changes +### 6. Make Your Changes - Follow the code standards outlined in [CLAUDE.md](CLAUDE.md) - Write tests for new functionality - Ensure your code passes linting: `npm run lint` and `npm run build` -### 6. Commit Your Changes +### 7. Commit Your Changes ```bash git add . git commit -m "feat: add your feature description" ``` -### 7. Push to Your Fork +### 8. Push to Your Fork ```bash git push origin your-branch-name ``` -### 8. Create a Pull Request +### 9. Create a Pull Request 1. Navigate to your fork on GitHub 2. Click "New Pull Request" From d062daf5b6bfb5e988bb241a6cf284f17cfd021c Mon Sep 17 00:00:00 2001 From: victrixhominum Date: Wed, 9 Jul 2025 16:05:06 +0100 Subject: [PATCH 15/15] build fixes --- jest.setup.ts | 4 +++- src/components/FormInput.tsx | 2 -- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/jest.setup.ts b/jest.setup.ts index 22c716e..811b52c 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -19,8 +19,10 @@ Object.defineProperty(window, 'matchMedia', { })), }); -// Mock IntersectionObserver which is not implemented in jsdom +// Mock IntersectionObserver, which is not implemented in jsdom, +// @ts-expect-error This is used for mocks in websockets only global.IntersectionObserver = class IntersectionObserver { + // eslint-disable-next-line @typescript-eslint/no-unused-vars constructor(callback: IntersectionObserverCallback) {} disconnect() { return null; diff --git a/src/components/FormInput.tsx b/src/components/FormInput.tsx index bfcc829..607bb71 100644 --- a/src/components/FormInput.tsx +++ b/src/components/FormInput.tsx @@ -9,7 +9,6 @@ interface FormInputProps { maxLength?: number; disabled?: boolean; className?: string; - onKeyPress?: (e: React.KeyboardEvent) => void; onKeyDown?: (e: React.KeyboardEvent) => void; } @@ -24,7 +23,6 @@ export default function FormInput({ maxLength, disabled = false, className = "", - onKeyPress, onKeyDown, }: FormInputProps) { return (