diff --git a/api/languages/index.ts b/api/languages/index.ts index 36ff84e..53de17a 100644 --- a/api/languages/index.ts +++ b/api/languages/index.ts @@ -1,9 +1,9 @@ import type { VercelRequest, VercelResponse } from "@vercel/node"; -import { parseQueryParams, type QueryParams } from "../../src/utils/params.js"; +import { parseQueryParams, type QueryParams } from "github-top-languages-lib/utils/params.js"; +import { generateChartData } from "github-top-languages-lib/render/chart.js"; +import { renderSvg } from "github-top-languages-lib/render/svg.js"; +import { renderError } from "github-top-languages-lib/render/error.js"; import { fetchLanguageData, processLanguageData } from "../../src/services/github.js"; -import { generateChartData } from "../../src/render/chart.js"; -import { renderSvg } from "../../src/render/svg.js"; -import { renderError } from "../../src/render/error.js"; export default async function handler( req: VercelRequest, diff --git a/package-lock.json b/package-lock.json index 05df995..92ee69a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "github-top-languages", "version": "0.0.1", "license": "MIT", + "dependencies": { + "github-top-languages-lib": "^1.0.0" + }, "devDependencies": { "@types/node": "^25.3.1", "@vercel/node": "^5.7.17", @@ -17,9 +20,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -27,9 +30,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -37,13 +40,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", - "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -53,14 +56,14 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -676,14 +679,14 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@tybys/wasm-util": "^0.10.1" + "@tybys/wasm-util": "^0.10.2" }, "funding": { "type": "github", @@ -733,9 +736,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.129.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.129.0.tgz", - "integrity": "sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg==", + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", "dev": true, "license": "MIT", "funding": { @@ -754,9 +757,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0.tgz", - "integrity": "sha512-TWMZnRLMe63C2Lhyicviu7ZHaU4kxa6PS3rofvc9GmcvptzNN11BcfQ4Sl7MwTOsisQoa2keB/EBdNCAnUo8vA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", "cpu": [ "arm64" ], @@ -771,9 +774,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0.tgz", - "integrity": "sha512-6XcD+8k0gPVItNagEw78/qqcBDwKcwDYS8V2hRmVsfUSIrd8cWe/CBvRDI5toqFyPfj+FJr6t8U6Xj2P2prEew==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", "cpu": [ "arm64" ], @@ -788,9 +791,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0.tgz", - "integrity": "sha512-iN/tWVXRQDWvmZlKdceP1Dwug9GDpEymhb9p4xnEe6zvCg5lFmzVljl+1qR1NVx3yfGpr2Na+CuLmv5IU8uzfQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", "cpu": [ "x64" ], @@ -805,9 +808,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0.tgz", - "integrity": "sha512-jjQMDvvwSOuhOwMszD/klSOjyWMM3zI64hWTj9KT5x4MxRbZAf+7vLQ6qouRhtsLVFHr3f0ILaJAfgENPiQdAQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", "cpu": [ "x64" ], @@ -822,9 +825,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0.tgz", - "integrity": "sha512-d//Dtg2x6/m3mbV64yUGNnDGNZaDGRpDLLNGerHQUVObuNaIQaaDp25yUiqGXtHEXX+NP2d0wAlmKgpYgIAJ2A==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", "cpu": [ "arm" ], @@ -839,9 +842,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0.tgz", - "integrity": "sha512-n7Ofp0mx+aB2cC+Sdy5YtMnXtY9lchnHbY+3Yt0uq9JsWQExf4f5Whu0tK0R8Jdc9S6RchTHjIFY7uc92puOVQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", "cpu": [ "arm64" ], @@ -859,9 +862,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0.tgz", - "integrity": "sha512-EIVjy2cgd7uuMMo94FVkBp7F6DhcZAUwNURkSG3RwUmvAXR6s0ISxM81U+IydcZByPG0pZIHsf1b6kTxoFDgJA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", "cpu": [ "arm64" ], @@ -879,9 +882,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0.tgz", - "integrity": "sha512-JEwwOPcwTLAcpDQlqSmjEmfs63xJnSiUNIGvLcDLUHCWK4XowpS/7c7tUsUH6uT/ct6bMUTdXKfI8967FYj6mg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", "cpu": [ "ppc64" ], @@ -899,9 +902,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0.tgz", - "integrity": "sha512-0wjCFhLrihtAubnT9iA0N++0pSV0z5Hg7tNGdNJ4RFaINceHadoF+kiFGyY1qSSNVIAZtLotG8Ju1bgDPkjnFA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", "cpu": [ "s390x" ], @@ -919,9 +922,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0.tgz", - "integrity": "sha512-Dfn7iak9BcMMePxcoJfpSbWqnEyrp/dRF63/8qW/eHBdOZov6x5aShLLEYGYdIeSJ6vMLK/XCVB+lGIxm41bQA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", "cpu": [ "x64" ], @@ -939,9 +942,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0.tgz", - "integrity": "sha512-5/utzzDmD/pD/bmuaUcbTf/sZYy0aztwIVlfpoW1fTjCZ0BaPOMVWGZL1zvgxyi7ZIVYWlxKONHmSbHuiOh8Jw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", "cpu": [ "x64" ], @@ -959,9 +962,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0.tgz", - "integrity": "sha512-ouJs8VcUomfLfpbUECqFMRqdV4x6aeAK3MA4m6vTrJJjKyWTV5KnxZx7Jd9G+GlDaQQxubcba00x16OyJ1meig==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", "cpu": [ "arm64" ], @@ -976,9 +979,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0.tgz", - "integrity": "sha512-E+oHKGiDA+lsKMmFtffDDw91EryDT7uJocrIuCHqhm6bCTM6xFK+3gaCkYOHfPwQr0cCNarSM2xaELoQDz9jJg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", "cpu": [ "wasm32" ], @@ -995,9 +998,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0.tgz", - "integrity": "sha512-yYK02n8Rngo+gbm1y6G0+7jk1sJ/2Wt7K0me0Y7k/ErBpyf+LJ2gFpqWVTcRV1rUepBlQRmpgWkTQCiiwrK0Ow==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", "cpu": [ "arm64" ], @@ -1012,9 +1015,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0.tgz", - "integrity": "sha512-14bpChMahXRRXiTwahSl+zzHPW6qQTXtkMuJBFlbo+pqSAews2d4BdCSHfrJ/MBsCZtpmTafsY+1QhBzitcmdg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", "cpu": [ "x64" ], @@ -1029,16 +1032,16 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0.tgz", - "integrity": "sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "dev": true, "license": "MIT" }, "node_modules/@rollup/pluginutils": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", - "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", "dev": true, "license": "MIT", "dependencies": { @@ -1122,19 +1125,19 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.7.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.7.0.tgz", - "integrity": "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==", + "version": "25.9.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.2.tgz", + "integrity": "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.21.0" + "undici-types": ">=7.24.0 <7.24.7" } }, "node_modules/@vercel/build-utils": { - "version": "13.22.1", - "resolved": "https://registry.npmjs.org/@vercel/build-utils/-/build-utils-13.22.1.tgz", - "integrity": "sha512-oYzvl9Xh0woTHQrGA9lKVDj1K1+MdfPPnerNwlQtldbsCDez55sxadGxvjCBmkVmNhTf+rkkQzSUe6uz0dFhBA==", + "version": "13.29.0", + "resolved": "https://registry.npmjs.org/@vercel/build-utils/-/build-utils-13.29.0.tgz", + "integrity": "sha512-Az9gTLZpndmQouuKCzwrKmMrOUxqfWJf1KfEVF7y59RvAJlflCn3RZ5iajIZgo2FqoAmCBGIgd0sovWWSXBWtw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1151,16 +1154,16 @@ "license": "MIT" }, "node_modules/@vercel/error-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vercel/error-utils/-/error-utils-2.1.0.tgz", - "integrity": "sha512-DiJcXBOB9N6QM4d7hYPM9Ck/AUjzBl58XNQPxS74o7CuvIanjzrGgygP/70VsyEASeIJMazk1LrhwcNTR/eZGQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@vercel/error-utils/-/error-utils-2.2.0.tgz", + "integrity": "sha512-WFWiRxfPzoYWYifaj4thSKvAaZZwUOqD4k5GINRIgZgCiS2E3iAJbWbIsIZmkQdTecWFHcWGA6q48CjisgpOBA==", "dev": true, "license": "Apache-2.0" }, "node_modules/@vercel/nft": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-1.5.0.tgz", - "integrity": "sha512-IWTDeIoWhQ7ZtRO/JRKH+jhmeQvZYhtGPmzw/QGDY+wDCQqfm25P9yIdoAFagu4fWsK4IwZXDFIjrmp5rRm/sA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-1.10.0.tgz", + "integrity": "sha512-iLOW4fcsgkipfOh2Bw3wB38YDfxTlxr7+j4uFeui2OswkNT28jIitS/aMce7tS0mef1YPQ8zLIDYr3a0aahNrA==", "dev": true, "license": "MIT", "dependencies": { @@ -1185,9 +1188,9 @@ } }, "node_modules/@vercel/node": { - "version": "5.7.17", - "resolved": "https://registry.npmjs.org/@vercel/node/-/node-5.7.17.tgz", - "integrity": "sha512-oFjbg+3nM2AvUu5OQTJIyp5KkNv7riu3YMAEjklPLd1HPtDXHWX0MVHtRiUeAQe/7bkJw8VNmqSRjfprP+OmPA==", + "version": "5.8.15", + "resolved": "https://registry.npmjs.org/@vercel/node/-/node-5.8.15.tgz", + "integrity": "sha512-arTGDg9/6qXxcFqpGeM8zCFLEoji69T/d2GQS/GYbM2Zx9pnzXQCHwY/sMnB5QSmoWVPDVHRSUmno//G7npDOw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1195,10 +1198,10 @@ "@edge-runtime/primitives": "4.1.0", "@edge-runtime/vm": "3.2.0", "@types/node": "20.11.0", - "@vercel/build-utils": "13.22.1", - "@vercel/error-utils": "2.1.0", - "@vercel/nft": "1.5.0", - "@vercel/static-config": "3.3.0", + "@vercel/build-utils": "13.29.0", + "@vercel/error-utils": "2.2.0", + "@vercel/nft": "1.10.0", + "@vercel/static-config": "3.4.0", "async-listen": "3.0.0", "cjs-module-lexer": "1.2.3", "edge-runtime": "2.5.9", @@ -1249,9 +1252,9 @@ } }, "node_modules/@vercel/static-config": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@vercel/static-config/-/static-config-3.3.0.tgz", - "integrity": "sha512-GpS3tPwUeDJCkrKbMNtS2XLRFgfxTlN7YNUL+Bo23+fGolrDw6Oq79R3yvxTYgqRaJMGSEqC7iMw6mj6I5loxg==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@vercel/static-config/-/static-config-3.4.0.tgz", + "integrity": "sha512-wCq90CMUB//ggnFh77NQO1xaLFsS4LigQIqKrH6ohnr9Br/KI1FhlErx62WfCOuueWaW+LVsbLOqNXIUjK8t6A==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1261,14 +1264,14 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.6.tgz", - "integrity": "sha512-36l628fQ/9a/8ihy97eOtEnvWQEdqULQOJtcaxtoNq0G1w3Mxd4szSahOaMM9/NGyZ+hyKcMtIW/WIxq0XQViQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.8.tgz", + "integrity": "sha512-lt3kovsyHwYe00wq4D1ti0Z974fWj4NLp6siqiyEufUpyFwK9Yhi7rBhac9JL5aA0zoMrJqc4vYPZRUnI7l7nw==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.1.6", + "@vitest/utils": "4.1.8", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", @@ -1282,8 +1285,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "4.1.6", - "vitest": "4.1.6" + "@vitest/browser": "4.1.8", + "vitest": "4.1.8" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -1292,16 +1295,16 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.6.tgz", - "integrity": "sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz", + "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.6", - "@vitest/utils": "4.1.6", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -1310,13 +1313,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.6.tgz", - "integrity": "sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz", + "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.6", + "@vitest/spy": "4.1.8", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -1347,9 +1350,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.6.tgz", - "integrity": "sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz", + "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==", "dev": true, "license": "MIT", "dependencies": { @@ -1360,13 +1363,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.6.tgz", - "integrity": "sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz", + "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.6", + "@vitest/utils": "4.1.8", "pathe": "^2.0.3" }, "funding": { @@ -1374,14 +1377,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.6.tgz", - "integrity": "sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz", + "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.6", - "@vitest/utils": "4.1.6", + "@vitest/pretty-format": "4.1.8", + "@vitest/utils": "4.1.8", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -1390,9 +1393,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.6.tgz", - "integrity": "sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz", + "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==", "dev": true, "license": "MIT", "funding": { @@ -1400,13 +1403,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.6.tgz", - "integrity": "sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz", + "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.6", + "@vitest/pretty-format": "4.1.8", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -1492,9 +1495,9 @@ } }, "node_modules/ast-v8-to-istanbul": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz", - "integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.4.tgz", + "integrity": "sha512-0bC0/4bTSrnwdhU3IsZDwEdojvuPrSg59OYZfKsLRtJZ0u8VBx9DebfqqG8bRdCC0I7vjgxmPi41P0lpkhJHtA==", "dev": true, "license": "MIT", "dependencies": { @@ -1907,6 +1910,12 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/github-top-languages-lib": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/github-top-languages-lib/-/github-top-languages-lib-1.0.0.tgz", + "integrity": "sha512-zeuw3sqpFG2RVdraWjZZu7fXOsEysShR9VodeUorB2zFb6Zp19Y9d95q2HLhO7wXce/XafJfh2we5ikoPK1wjw==", + "license": "MIT" + }, "node_modules/glob": { "version": "13.0.6", "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", @@ -2373,9 +2382,9 @@ } }, "node_modules/lru-cache": { - "version": "11.3.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", - "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -2393,13 +2402,13 @@ } }, "node_modules/magicast": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", - "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", + "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", + "@babel/parser": "^7.29.3", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } @@ -2618,15 +2627,18 @@ } }, "node_modules/obug": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", - "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.2.tgz", + "integrity": "sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==", "dev": true, "funding": [ "https://github.com/sponsors/sxzz", "https://opencollective.com/debug" ], - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } }, "node_modules/parse-ms": { "version": "2.1.0", @@ -2705,9 +2717,9 @@ } }, "node_modules/postcss": { - "version": "8.5.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", - "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "dev": true, "funding": [ { @@ -2725,7 +2737,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -2819,14 +2831,14 @@ } }, "node_modules/rolldown": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0.tgz", - "integrity": "sha512-yD986aXDESFGS95spT1LAv0jssywP4npMEjmMHyN2/5+eE8qQJUype2AaKkRiLgBgyD0LFlubwAht7VmY8rGoA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.129.0", - "@rolldown/pluginutils": "1.0.0" + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { "rolldown": "bin/cli.mjs" @@ -2835,21 +2847,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0", - "@rolldown/binding-darwin-arm64": "1.0.0", - "@rolldown/binding-darwin-x64": "1.0.0", - "@rolldown/binding-freebsd-x64": "1.0.0", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0", - "@rolldown/binding-linux-arm64-gnu": "1.0.0", - "@rolldown/binding-linux-arm64-musl": "1.0.0", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0", - "@rolldown/binding-linux-s390x-gnu": "1.0.0", - "@rolldown/binding-linux-x64-gnu": "1.0.0", - "@rolldown/binding-linux-x64-musl": "1.0.0", - "@rolldown/binding-openharmony-arm64": "1.0.0", - "@rolldown/binding-wasm32-wasi": "1.0.0", - "@rolldown/binding-win32-arm64-msvc": "1.0.0", - "@rolldown/binding-win32-x64-msvc": "1.0.0" + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" } }, "node_modules/run-parallel": { @@ -2877,9 +2889,9 @@ } }, "node_modules/semver": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", - "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", "dev": true, "license": "ISC", "bin": { @@ -2960,9 +2972,9 @@ } }, "node_modules/tar": { - "version": "7.5.15", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.15.tgz", - "integrity": "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==", + "version": "7.5.16", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.16.tgz", + "integrity": "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -3000,9 +3012,9 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", - "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", "dev": true, "license": "MIT", "engines": { @@ -3010,9 +3022,9 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { @@ -3127,9 +3139,9 @@ } }, "node_modules/undici-types": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.21.0.tgz", - "integrity": "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "dev": true, "license": "MIT" }, @@ -3144,17 +3156,17 @@ } }, "node_modules/vite": { - "version": "8.0.12", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.12.tgz", - "integrity": "sha512-w2dDofOWv2QB09ZITZBsvKTVAlYvPR4IAmrY/v0ir9KvLs0xybR7i48wxhM1/oyBWO34wPns+bPGw5ZrZqDpZg==", + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", - "postcss": "^8.5.14", - "rolldown": "1.0.0", - "tinyglobby": "^0.2.16" + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" @@ -3222,19 +3234,19 @@ } }, "node_modules/vitest": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.6.tgz", - "integrity": "sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz", + "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.6", - "@vitest/mocker": "4.1.6", - "@vitest/pretty-format": "4.1.6", - "@vitest/runner": "4.1.6", - "@vitest/snapshot": "4.1.6", - "@vitest/spy": "4.1.6", - "@vitest/utils": "4.1.6", + "@vitest/expect": "4.1.8", + "@vitest/mocker": "4.1.8", + "@vitest/pretty-format": "4.1.8", + "@vitest/runner": "4.1.8", + "@vitest/snapshot": "4.1.8", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -3262,12 +3274,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.6", - "@vitest/browser-preview": "4.1.6", - "@vitest/browser-webdriverio": "4.1.6", - "@vitest/coverage-istanbul": "4.1.6", - "@vitest/coverage-v8": "4.1.6", - "@vitest/ui": "4.1.6", + "@vitest/browser-playwright": "4.1.8", + "@vitest/browser-preview": "4.1.8", + "@vitest/browser-webdriverio": "4.1.8", + "@vitest/coverage-istanbul": "4.1.8", + "@vitest/coverage-v8": "4.1.8", + "@vitest/ui": "4.1.8", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" diff --git a/package.json b/package.json index e9e9c53..e0cfebe 100644 --- a/package.json +++ b/package.json @@ -13,12 +13,13 @@ "license": "MIT", "type": "module", "devDependencies": { - "@types/node": "^25.3.1", - "@vercel/node": "^5.7.17", - "@vitest/coverage-v8": "^4.0.17", "typescript": "^5.0.0", - "vitest": "^4.0.17" + "@vercel/node": "^5.7.17", + "@types/node": "^25.3.1", + "vitest": "^4.0.17", + "@vitest/coverage-v8": "^4.0.17" }, + "dependencies": { "github-top-languages-lib": "^1.0.0" }, "overrides": { "minimatch": "10.2.5", "smol-toml": "1.6.1", diff --git a/src/charts/donut.ts b/src/charts/donut.ts deleted file mode 100644 index 532f1b6..0000000 --- a/src/charts/donut.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { Language, Theme, ChartResult } from "../types.js"; -import { resolveLayout, calculateChartCenter, calculateLegendStartX } from "./layout.js"; -import { DONUT_GEOMETRY } from "../constants/geometry.js"; -import { createDonutSegments } from "./geometry.js"; -import { createLegend } from "./legend.js"; - -export function generateDonutChart( - normalizedLanguages: Language[], - selectedTheme: Theme, - width: number, - stroke: boolean -): ChartResult { - const { isShifted, useStroke } = resolveLayout(normalizedLanguages.length, stroke); - - const chartX = calculateChartCenter(width, isShifted); - const legendStartX = calculateLegendStartX(chartX, DONUT_GEOMETRY.OUTER_RADIUS); - - const segments = createDonutSegments(normalizedLanguages, chartX, DONUT_GEOMETRY, [...selectedTheme.colours], useStroke); - const legend = createLegend(normalizedLanguages, isShifted, selectedTheme, legendStartX, useStroke); - - return { segments, legend }; -} diff --git a/src/charts/geometry.ts b/src/charts/geometry.ts deleted file mode 100644 index 4043b39..0000000 --- a/src/charts/geometry.ts +++ /dev/null @@ -1,88 +0,0 @@ -import type { Point, Language, Geometry } from "../types.js" -import { FULL_CIRCLE_ANGLE } from "../constants/geometry.js"; - -export const polarToCartesian = ( - cx: number, - cy: number, - r: number, - angleDeg: number -): Point => { - const angleRad = (angleDeg - 90) * Math.PI / 180; - return { - x: cx + (r * Math.cos(angleRad)), - y: cy + (r * Math.sin(angleRad)) - }; -}; - -export const describeSegment = ( - cx: number, - cy: number, - innerR: number, - outerR: number, - startAngle: number, - endAngle: number -): string => { - const angleDiff = endAngle - startAngle - - if (angleDiff >= 360 || angleDiff <= -360) { - const midAngle = startAngle + 180; - const firstHalf = describeSegment(cx, cy, innerR, outerR, startAngle, midAngle); - const secondHalf = describeSegment(cx, cy, innerR, outerR, midAngle, endAngle); - return firstHalf + ' ' + secondHalf; - } - - const startOuter = polarToCartesian(cx, cy, outerR, endAngle); - const endOuter = polarToCartesian(cx, cy, outerR, startAngle); - const largeArcFlag = endAngle - startAngle <= 180 ? '0' : '1'; - - if (innerR === 0) { - return ` - M ${cx} ${cy} - L ${startOuter.x} ${startOuter.y} - A ${outerR} ${outerR} 0 ${largeArcFlag} 0 ${endOuter.x} ${endOuter.y} - Z - `.trim(); - } - - const startInner = polarToCartesian(cx, cy, innerR, startAngle); - const endInner = polarToCartesian(cx, cy, innerR, endAngle); - - return ` - M ${startOuter.x} ${startOuter.y} - A ${outerR} ${outerR} 0 ${largeArcFlag} 0 ${endOuter.x} ${endOuter.y} - L ${startInner.x} ${startInner.y} - A ${innerR} ${innerR} 0 ${largeArcFlag} 1 ${endInner.x} ${endInner.y} - Z - `.trim(); -}; - -export const createDonutSegments = ( - languages: Language[], - cx: number, - geometry: Geometry, - colours: string[], - stroke: boolean -): string => { - let currentAngle = -0.1; - - return languages.map((lang, i) => { - let angle = (lang.pct / 100) * 360; - - const segmentAngle = Math.min(currentAngle + angle + 0.1, FULL_CIRCLE_ANGLE); - const path = describeSegment( - cx, - geometry.CENTER_Y, - geometry.INNER_RADIUS, - geometry.OUTER_RADIUS, - currentAngle, - segmentAngle - ); - - currentAngle += angle; - const fillColour = colours[i % colours.length]; - const strokeAttr = stroke - ? ` stroke="#000" stroke-width="0.5" stroke-linejoin="round"` - : ` stroke="${fillColour}" stroke-width="0.2"`; - return ``; - }).join(''); -} diff --git a/src/charts/layout.ts b/src/charts/layout.ts deleted file mode 100644 index b16014a..0000000 --- a/src/charts/layout.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { LEGEND_SHIFT_THRESHOLD, LEGEND_STYLES, CHART_MARGIN_RIGHT } from "../constants/styles.js"; - -export function resolveLayout(count: number, stroke: boolean) { - return { - isShifted: count > LEGEND_SHIFT_THRESHOLD, - useStroke: count > 1 ? stroke : false - }; -} - -export function calculateChartCenter(width: number, isShifted: boolean): number { - const legendWidth = isShifted - ? LEGEND_STYLES.COLUMN_WIDTH * 2 - : LEGEND_STYLES.WIDTH; - return (width - legendWidth - CHART_MARGIN_RIGHT) / 2; -} - -export function calculateLegendStartX(chartCenterX: number, radius: number): number { - return chartCenterX + radius + CHART_MARGIN_RIGHT; -} diff --git a/src/charts/legend.ts b/src/charts/legend.ts deleted file mode 100644 index 6fd7d09..0000000 --- a/src/charts/legend.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { LEGEND_STYLES } from "../constants/styles.js"; -import type { Language, Theme } from "../types.js"; - -export function createLegend( - languages: Language[], - isShifted: boolean, - selectedTheme: Theme, - legendStartX: number, - stroke: boolean -): string { - const numLangs = languages.length; - - return languages.map((lang, i) => { - let x: number, y: number; - - if (!isShifted) { - x = legendStartX; - y = LEGEND_STYLES.START_Y + i * LEGEND_STYLES.ROW_HEIGHT; - } else { - const half = Math.ceil(numLangs / 2); - const col = Math.floor(i / half); - const row = i % half; - - x = legendStartX + col * LEGEND_STYLES.COLUMN_WIDTH; - y = LEGEND_STYLES.START_Y + row * LEGEND_STYLES.ROW_HEIGHT; - } - - const fill = selectedTheme.colours[i]; - const strokeAttr = stroke - ? ` stroke="#000" stroke-width="0.5" stroke-linejoin="round"` - : ``; - - return ` - - - ${lang.lang} ${lang.pct.toFixed(1)}% - - `; - }).join(''); -} diff --git a/src/charts/pie.ts b/src/charts/pie.ts deleted file mode 100644 index ce9fa95..0000000 --- a/src/charts/pie.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { Language, Theme, ChartResult } from "../types.js"; -import { resolveLayout, calculateChartCenter, calculateLegendStartX } from "./layout.js"; -import { PIE_GEOMETRY } from "../constants/geometry.js"; -import { createDonutSegments } from "./geometry.js"; -import { createLegend } from "./legend.js"; - -export function generatePieChart( - normalizedLanguages: Language[], - selectedTheme: Theme, - width: number, - stroke: boolean -): ChartResult { - const { isShifted, useStroke } = resolveLayout(normalizedLanguages.length, stroke); - - const chartX = calculateChartCenter(width, isShifted); - const legendStartX = calculateLegendStartX(chartX, PIE_GEOMETRY.OUTER_RADIUS); - - const segments = createDonutSegments(normalizedLanguages, chartX, PIE_GEOMETRY, [...selectedTheme.colours], useStroke); - const legend = createLegend(normalizedLanguages, isShifted, selectedTheme, legendStartX, useStroke); - - return { segments, legend }; -} diff --git a/src/constants/config.ts b/src/constants/config.ts deleted file mode 100644 index 11c6024..0000000 --- a/src/constants/config.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const DEFAULT_CONFIG = { - TITLE: "Top Languages", - WIDTH: 400, - HEIGHT: 300, - COUNT: 8, - MIN_WIDTH: 400 -} as const; - -export const REFRESH_INTERVAL = 1000 * 60 * 60; -export const MAX_COUNT = 16; diff --git a/src/constants/geometry.ts b/src/constants/geometry.ts deleted file mode 100644 index acc9f80..0000000 --- a/src/constants/geometry.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const FULL_CIRCLE_ANGLE = 359.9999; - -const BASE_GEOMETRY = { - CENTER_Y: 170, - OUTER_RADIUS: 80, -} as const; - -export const DONUT_GEOMETRY = { ...BASE_GEOMETRY, INNER_RADIUS: 50 } as const; -export const PIE_GEOMETRY = { ...BASE_GEOMETRY, INNER_RADIUS: 0 } as const; diff --git a/src/constants/styles.ts b/src/constants/styles.ts deleted file mode 100644 index 704ff50..0000000 --- a/src/constants/styles.ts +++ /dev/null @@ -1,23 +0,0 @@ -export const TITLE_STYLES = { - TEXT_Y: 30, - FONT_SIZE: 24 -} as const; - -export const LEGEND_STYLES = { - START_Y: 80, - ROW_HEIGHT: 25, - SQUARE_SIZE: 12, - SQUARE_RADIUS: 2, - FONT_SIZE: 11, - WIDTH: 130, - COLUMN_WIDTH: 105 -} as const; - -export const ERROR_STYLES = { - TEXT_Y: 100, - FONT_SIZE: 18, - COLOUR: "#ff6b6b" -} as const; - -export const LEGEND_SHIFT_THRESHOLD = 8; -export const CHART_MARGIN_RIGHT = 20; diff --git a/src/constants/themes.ts b/src/constants/themes.ts deleted file mode 100644 index ad938a8..0000000 --- a/src/constants/themes.ts +++ /dev/null @@ -1,68 +0,0 @@ -export const THEMES = { - default: { - bg: "#0d1117", - text: "#ffffff", - colours: [ - "#A8D5Ba", - "#FFD6A5", - "#FFAAA6", - "#D0CFCF", - "#CBAACB", - "#FFE156", - "#96D5E9", - "#F3B0C3", - "#B4A7D6", - "#FFB6B9", - "#A3E4D7", - "#F8B88B", - "#C9E4CA", - "#FAD7A0", - "#AED6F1", - "#D7BDE2" - ] - }, - light: { - bg: "#ffffff", - text: "#2f2f2f", - colours: [ - "#2ecc71", - "#3498db", - "#e74c3c", - "#f39c12", - "#9b59b6", - "#1abc9c", - "#e67e22", - "#34495e", - "#16a085", - "#c0392b", - "#8e44ad", - "#27ae60", - "#d35400", - "#2980b9", - "#7f8c8d", - "#f1c40f" - ] - }, - dark: { - bg: "#1a1a1a", - text: "#ccd6f6", - colours: [ - "#ff6b6b", - "#4ecdc3", - "#45b7d1", - "#ffa07a", - "#98d8c8", - "#f7dc6f", - "#bb8fce", - "#64ffda", - "#85c1e2", - "#ff8a80", - "#a7ffeb", - "#ffd54f", - "#ea80fc", - "#80d8ff", - "#ffab91", - "#b9f6ca" - ] - } -} as const; diff --git a/src/constants/types.ts b/src/constants/types.ts deleted file mode 100644 index 835e8aa..0000000 --- a/src/constants/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const VALID_TYPES = [ - "donut", - "pie" -] as const; diff --git a/src/render/chart.ts b/src/render/chart.ts deleted file mode 100644 index 9cf1f74..0000000 --- a/src/render/chart.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { Language, Theme, ChartType, ChartResult } from "../types.js"; -import { generateDonutChart } from "../charts/donut.js"; -import { generatePieChart } from "../charts/pie.js"; - -const CHART_GENERATORS: Record ChartResult> = { - donut: generateDonutChart, - pie: generatePieChart, -} - -export function generateChartData( - data: Language[], - theme: Theme, - chartType: ChartType, - width: number, - stroke: boolean -): ChartResult { - const generator = CHART_GENERATORS[chartType] || CHART_GENERATORS.donut; - return generator(data, theme, width, stroke); -} diff --git a/src/render/error.ts b/src/render/error.ts deleted file mode 100644 index 8a2b715..0000000 --- a/src/render/error.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { Theme } from "../types.js"; -import { THEMES } from "../constants/themes.js"; -import { ERROR_STYLES } from "../constants/styles.js" -import { sanitize } from "../utils/sanitize.js"; - -export function renderError( - message: string, - width: number, - height: number, - selectedTheme?: Theme -): string { - const background = selectedTheme?.bg || THEMES.default.bg; - const maxLen = 40; - const truncated = message.length > maxLen - ? sanitize(message.slice(0, maxLen)) + "..." - : sanitize(message); - return ` - - - - Error: ${truncated} - - - `.trim(); -} diff --git a/src/render/svg.ts b/src/render/svg.ts deleted file mode 100644 index 9a78ad8..0000000 --- a/src/render/svg.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { TITLE_STYLES } from "../constants/styles.js" - -export function renderSvg( - width: number, - height: number, - background: string, - segments: string, - legend: string, - title: string | null, - textColour: string -): string { - const titleElement = title ? ` - - ${title} - - ` : ''; - - return ` - - - ${titleElement} - ${segments} - ${legend} - - `.trim(); -} diff --git a/src/services/github.ts b/src/services/github.ts index 7c7ab74..aaaa9c9 100644 --- a/src/services/github.ts +++ b/src/services/github.ts @@ -1,5 +1,5 @@ -import { REFRESH_INTERVAL } from "../constants/config.js"; -import type { Language } from "../types.js"; +import { REFRESH_INTERVAL } from "github-top-languages-lib/constants/config.js"; +import type { Language } from "github-top-languages-lib/types.js"; type LanguageBytes = Record; diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index fafc211..0000000 --- a/src/types.ts +++ /dev/null @@ -1,28 +0,0 @@ -export type Point = { - x: number; - y: number; -}; - -export type Language = { - lang: string; - pct: number; -}; - -export type Geometry = { - CENTER_Y: number; - INNER_RADIUS: number; - OUTER_RADIUS: number; -}; - -export type ChartType = "donut" | "pie"; - -export type ChartResult = { - segments: string; - legend: string; -}; - -export type Theme = { - readonly colours: readonly string[]; - readonly text: string; - readonly bg: string; -}; diff --git a/src/utils/params.ts b/src/utils/params.ts deleted file mode 100644 index aa431b4..0000000 --- a/src/utils/params.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { ChartType } from "../types.js"; -import { sanitize } from "./sanitize.js"; -import { VALID_TYPES } from "../constants/types.js"; -import { DEFAULT_CONFIG, MAX_COUNT } from "../constants/config.js"; -import { THEMES } from "../constants/themes.js"; - -export type QueryParams = Record; - -const parseIntSafe = ( - val: string | undefined, - fallback: number -): number => { - const parsed = Number.parseInt(val ?? '', 10); - return Number.isNaN(parsed) ? fallback : parsed; -} - -const normalizeHex = (val: string) => `#${val.replace(/^#/, '')}`; - -export function parseQueryParams(query: QueryParams) { - const baseTheme = THEMES[query["theme"] as keyof typeof THEMES] ?? THEMES.default; - const count = parseIntSafe(query["count"], DEFAULT_CONFIG.COUNT); - - const customColours: string[] = [...baseTheme.colours]; - for (let i = 1; i <= MAX_COUNT; i++) { - const colourVal = query[`c${i}`]; - if(colourVal) customColours[i - 1] = normalizeHex(colourVal); - } - - const typeParam = query["type"] as ChartType | undefined; - const chartType: ChartType = VALID_TYPES.some(t => t === typeParam) ? typeParam! : "donut"; - - return { - chartType, - chartTitle: query["hide_title"] === "true" ? '' : sanitize(query["title"] ?? DEFAULT_CONFIG.TITLE), - width: Math.max(parseIntSafe(query["width"], DEFAULT_CONFIG.WIDTH), DEFAULT_CONFIG.MIN_WIDTH), - height: parseIntSafe(query["height"], DEFAULT_CONFIG.HEIGHT), - count: Math.min(Math.max(count, 1), MAX_COUNT), - selectedTheme: { - bg: THEMES[query["bg"] as keyof typeof THEMES]?.bg ?? (query["bg"] ? normalizeHex(query["bg"]) : baseTheme.bg), - text: query["text"] ? normalizeHex(query["text"]) : baseTheme.text, - colours: customColours - }, - stroke: query["stroke"] === "true", - useTestData: query["test"] === "true", - errorTest: sanitize(query["error"] ?? '') - } -} diff --git a/src/utils/sanitize.ts b/src/utils/sanitize.ts deleted file mode 100644 index e527af5..0000000 --- a/src/utils/sanitize.ts +++ /dev/null @@ -1,14 +0,0 @@ -const ESCAPE_MAP = { - '<': '<', - '>': '>', - '&': '&', - '"': '"', - "'": ''', -} as const; - -export const sanitize = (str: unknown): string => { - if (typeof str !== "string") return ''; - return str.replace(/[<>&"']/g, (m: string): string => - ESCAPE_MAP[m as keyof typeof ESCAPE_MAP] - ); -}; diff --git a/tests/api/languages/index.test.ts b/tests/api/languages/index.test.ts index db74b4e..68a0a97 100644 --- a/tests/api/languages/index.test.ts +++ b/tests/api/languages/index.test.ts @@ -1,18 +1,18 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import type { VercelRequest, VercelResponse } from "@vercel/node"; -import type { ChartResult } from "../../../src/types.js"; +import type { ChartResult } from "github-top-languages-lib/types.js"; +import { parseQueryParams } from "github-top-languages-lib/utils/params.js"; +import { generateChartData } from "github-top-languages-lib/render/chart.js"; +import { renderSvg } from "github-top-languages-lib/render/svg.js"; +import { renderError } from "github-top-languages-lib/render/error.js"; import handler from "../../../api/languages/index.js"; -import { parseQueryParams } from "../../../src/utils/params.js"; import { fetchLanguageData, processLanguageData } from "../../../src/services/github.js"; -import { generateChartData } from "../../../src/render/chart.js"; -import { renderSvg } from "../../../src/render/svg.js"; -import { renderError } from "../../../src/render/error.js"; -vi.mock("../../../src/utils/params.js"); +vi.mock("github-top-languages-lib/utils/params.js"); +vi.mock("github-top-languages-lib/render/chart.js"); +vi.mock("github-top-languages-lib/render/svg.js"); +vi.mock("github-top-languages-lib/render/error.js"); vi.mock("../../../src/services/github.js"); -vi.mock("../../../src/render/chart.js"); -vi.mock("../../../src/render/svg.js"); -vi.mock("../../../src/render/error.js"); describe("handler", () => { let req: VercelRequest; diff --git a/tests/charts/donut.test.ts b/tests/charts/donut.test.ts deleted file mode 100644 index c94284c..0000000 --- a/tests/charts/donut.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { describe, it, expect, vi } from "vitest"; -import type { Theme } from "../../src/types.js"; -import { LEGEND_SHIFT_THRESHOLD } from "../../src/constants/styles.js"; -import { generateDonutChart } from "../../src/charts/donut.js"; -import { createDonutSegments } from "../../src/charts/geometry.js"; -import { createLegend } from "../../src/charts/legend.js"; - -vi.mock("../../src/charts/geometry.js", () => ({ - createDonutSegments: vi.fn(() => ``) -})); - -vi.mock("../../src/charts/legend.js", () => ({ - createLegend: vi.fn(() => "mockLegend") -})); - -const mockCreateDonutSegments = vi.mocked(createDonutSegments); -const mockCreateLegend = vi.mocked(createLegend); - -describe("generateDonutChart", () => { - const theme: Theme = { colours: ["#f00", "#0f0"], text: "#333", bg: "#fff" }; - - it("returns segments and legend", () => { - const langs = [{ lang: "JS", pct: 100 }]; - const result = generateDonutChart(langs, theme, 800, false); - expect(result).toHaveProperty("segments"); - expect(result).toHaveProperty("legend"); - expect(mockCreateDonutSegments).toHaveBeenCalled(); - expect(mockCreateLegend).toHaveBeenCalled(); - }); - - it("passes isShifted=false when below threshold", () => { - const langs = Array.from({ length: LEGEND_SHIFT_THRESHOLD }, (_, i) => ({ - lang: `L${i}`, pct: 100 / LEGEND_SHIFT_THRESHOLD - })); - generateDonutChart(langs, theme, 800, false); - const legendCall = mockCreateLegend.mock.calls.at(-1) ?? []; - expect(legendCall[1]).toBe(false); - }); - - it("passes isShifted=true when above threshold", () => { - const langs = Array.from({ length: LEGEND_SHIFT_THRESHOLD + 1 }, (_, i) => ({ - lang: `L${i}`, pct: 100 / (LEGEND_SHIFT_THRESHOLD + 1) - })); - - generateDonutChart(langs, theme, 800, false); - const legendCall = mockCreateLegend.mock.calls.at(-1) ?? []; - expect(legendCall[1]).toBe(true); - }); - - it("calculates positions based on width", () => { - const langs = [{ lang: "Python", pct: 100 }]; - generateDonutChart(langs, theme, 1000, false); - const segmentCall = mockCreateDonutSegments.mock.calls.at(-1); - const legendCall = mockCreateLegend.mock.calls.at(-1) ?? []; - expect(typeof segmentCall![1]).toBe("number"); - expect(typeof legendCall![3]).toBe("number"); - }); - - it("passes theme to both segments and legend", () => { - const langs = [{ lang: "HTML", pct: 100 }]; - generateDonutChart(langs, theme, 800, false); - const segmentsCall = mockCreateDonutSegments.mock.calls.at(-1)!; - const legendCall = mockCreateLegend.mock.calls.at(-1) ?? []; - expect(segmentsCall[3]).toEqual(theme.colours); - expect(legendCall[2]).toBe(theme); - }); -}); diff --git a/tests/charts/geometry.test.ts b/tests/charts/geometry.test.ts deleted file mode 100644 index 31a0665..0000000 --- a/tests/charts/geometry.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { describe, it, expect } from "vitest"; -import type { Language } from "../../src/types.js"; -import { polarToCartesian, describeSegment, createDonutSegments } from "../../src/charts/geometry.js"; - -const mockGeometry = { CENTER_Y: 100, INNER_RADIUS: 30, OUTER_RADIUS: 50 }; - -describe("donut geometry", () => { - it("polarToCartesian: quadrants correct", () => { - expect(polarToCartesian(100, 100, 50, 0 )).toEqual({ x: 100, y: 50 }); // Top - expect(polarToCartesian(100, 100, 50, 90 )).toEqual({ x: 150, y: 100 }); // Right - expect(polarToCartesian(100, 100, 50, 180)).toEqual({ x: 100, y: 150 }); // Bottom - expect(polarToCartesian(100, 100, 50, 270)).toEqual({ x: 50, y: 100 }); // Left - }); - - it("describeSegment: small arc path", () => { - const path = describeSegment(100, 100, 30, 50, 0, 90); - expect(path).toMatch(/^M \d+\.?\d* \d+\.?\d*/); - expect(path).toMatch(/A 50 50 0 0 0/); - expect(path).toMatch(/Z$/); - }); - - it("describeSegment: filled pie starts at center", () => { - const path = describeSegment(100, 100, 0, 50, 0, 90); - expect(path.startsWith("M 100 100")).toBe(true); - }); - - it("describeSegment: segments have stroke when enabled", () => { - const langs: Language[] = [{ lang: "JS", pct: 50 }]; - const paths = createDonutSegments( - langs, - 100, - mockGeometry, - ["#f00"], - true - ); - expect(paths).toMatch(/stroke="#000"/); - }); - - it("createDonutSegments: single lang full circle", () => { - const langs: Language[] = [{ lang: "JS", pct: 100 }]; - const paths = createDonutSegments( - langs, - 100, - mockGeometry, - ["#f00"], - false - ); - expect(paths).toMatch(/fill="#f00"/); - }); - - it("createDonutSegments: multi-lang sums to 360°", () => { - const langs: Language[] = [ - { lang: "JS", pct: 33 }, - { lang: "TS", pct: 33 }, - { lang: "PY", pct: 34 } - ]; - const paths = createDonutSegments( - langs, - 100, - mockGeometry, - ["#f00", "#0f0", "#00f"], - false - ); - expect(paths.split("/>").length - 1).toBe(3); - }); -}); diff --git a/tests/charts/legend.test.ts b/tests/charts/legend.test.ts deleted file mode 100644 index 969989e..0000000 --- a/tests/charts/legend.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { describe, it, expect } from "vitest"; -import type { Theme } from "../../src/types.js"; -import { LEGEND_STYLES } from "../../src/constants/styles.js"; -import { createLegend } from "../../src/charts/legend.js"; - -const theme: Theme = { colours: ["#f00", "#0f0", "#00f"], text: "#333", bg: "#fff" }; - -describe("createLegend", () => { - it("single-column layout positions correctly", () => { - const langs = [ - { lang: "JavaScript", pct: 60 }, - { lang: "Python", pct: 40 } - ]; - const result = createLegend(langs, false, theme, 300, false); - expect(result).toContain(`x="300"`); - expect(result).toContain(`y="${LEGEND_STYLES.START_Y}"`); - expect(result).toContain(`y="${LEGEND_STYLES.START_Y + LEGEND_STYLES.ROW_HEIGHT}"`); - }); - - it("two-column layout when isShifted", () => { - const langs = Array.from({ length: 8 }, (_, i) => ({ - lang: `Lang${i}`, - pct: 12.5 - })); - const result = createLegend(langs, true, theme, 300, false); - expect(result).toContain(`x="300`); - expect(result).toContain(`x="${300 + LEGEND_STYLES.COLUMN_WIDTH}"`); - }); - - it("formats percentages to one decimal", () => { - const langs = [{ lang: "Rust", pct: 33.333 }]; - const result = createLegend(langs, false, theme, 300, false); - - expect(result).toContain("33.3%"); - expect(result).not.toContain("33.333"); - }); - - it("adds stroke attributes when stroke is enabled", () => { - const langs = [{ lang: "C#", pct: 100 }]; - const result = createLegend(langs, false, theme, 300, true); - - expect(result).toContain(`stroke="#000"`); - expect(result).toContain(`stroke-width="0.5"`); - }); - - it("generates rect and text for each language", () => { - const langs = [ - { lang: "C#", pct: 50 }, - { lang: "C++", pct: 50 } - ]; - const result = createLegend(langs, false, theme, 300, false); - expect(result.match(/ { - const langs = [{ lang: "Java", pct: 100 }]; - const result = createLegend(langs, false, theme, 300, false); - expect(result).toContain(`fill="#f00"`); - expect(result).toContain(`fill="${theme.text}"`); - }); -}); diff --git a/tests/charts/pie.test.ts b/tests/charts/pie.test.ts deleted file mode 100644 index 49ba4b6..0000000 --- a/tests/charts/pie.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { describe, it, expect, vi } from "vitest"; -import { generatePieChart } from "../../src/charts/pie.js"; -import { createDonutSegments } from "../../src/charts/geometry.js"; -import { LEGEND_SHIFT_THRESHOLD } from "../../src/constants/styles.js"; -import { createLegend } from "../../src/charts/legend.js"; - -vi.mock("../../src/charts/geometry.js", () => ({ - createDonutSegments: vi.fn(() => ``) -})); - -vi.mock("../../src/charts/legend.js", () => ({ - createLegend: vi.fn(() => `mockLegend`) -})); - -const mockCreateDonutSegments = vi.mocked(createDonutSegments); -const mockCreateLegend = vi.mocked(createLegend); - -describe("generatePieChart", () => { - const theme = { colours: ["#f00", "#0f0"], text: "#333", bg: "#fff" }; - - it("returns segments and legend", () => { - const langs = [{ lang: "JS", pct: 100 }]; - const result = generatePieChart(langs, theme, 800, false); - expect(result).toHaveProperty("segments"); - expect(result).toHaveProperty("legend"); - expect(mockCreateDonutSegments).toHaveBeenCalled(); - expect(mockCreateLegend).toHaveBeenCalled(); - }); - - it("passes INNER_RADIUS: 0 for filled pie", () => { - const langs = [{ lang: "Python", pct: 100 }]; - generatePieChart(langs, theme, 800, false); - const call = mockCreateDonutSegments.mock.calls.at(-1)!; - const geometry = call[2]; - expect(geometry.INNER_RADIUS).toBe(0); - }); - - it("shifts legend when above threshold", () => { - const langs = Array.from({ length: LEGEND_SHIFT_THRESHOLD + 1 }, (_, i) => ({ - lang: `L${i}`, pct: 100 / (LEGEND_SHIFT_THRESHOLD + 1) - })); - generatePieChart(langs, theme, 800, false); - const legendCall = mockCreateLegend.mock.calls.at(-1)!; - expect(legendCall[1]).toBe(true); - }); - - it("calculates positions based on width", () => { - const langs = [{ lang: "Rust", pct: 100 }]; - generatePieChart(langs, theme, 1000, false); - const segmentCall = mockCreateDonutSegments.mock.calls.at(-1)!; - const legendCall = mockCreateLegend.mock.calls.at(-1)!; - expect(typeof segmentCall[1]).toBe("number"); - expect(typeof legendCall[3]).toBe("number"); - }); -}); diff --git a/tests/render/chart.test.ts b/tests/render/chart.test.ts deleted file mode 100644 index f2c636f..0000000 --- a/tests/render/chart.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { describe, it, expect, vi } from "vitest"; -import type { ChartType, ChartResult, Language, Theme } from "../../src/types.js"; -import { generateChartData } from "../../src/render/chart.js"; -import { generateDonutChart } from "../../src/charts/donut.js"; -import { THEMES } from "../../src/constants/themes.js"; - -type MockChartResult = ChartResult & { - mockData: boolean; - data: Language[]; - theme: Theme; - width: number; -} - -vi.mock("../../src/charts/donut.js", () => ({ - generateDonutChart: vi.fn((data, theme, width, _stroke) => ({ - segments: "", - legend: "", - mockData: true, - data, - theme, - width - } as MockChartResult)) -})); - -describe("generateChartData", () => { - const data = [{ lang: "JavaScript", pct: 60 }]; - const theme = THEMES.default; - const chartType = "donut"; - const width = 400; - const stroke = false; - - it("should call donut generator when chartType is donut", () => { - const result = generateChartData(data, theme, chartType, width, stroke) as MockChartResult; - expect(generateDonutChart).toHaveBeenCalledWith(data, theme, width, stroke); - expect(result.data).toBe(data); - expect(result.theme).toBe(theme); - expect(result.width).toBe(width); - }); - - it("defaults to donut generator when chartType is undefined", () => { - generateChartData(data, theme, undefined as unknown as ChartType, width, stroke); - expect(generateDonutChart).toHaveBeenCalledWith(data, theme, width, stroke); - }); - - it("defaults to donut generator for unrecognized chartType", () => { - generateChartData(data, theme, "bigbadwolf" as ChartType, width, stroke); - expect(generateDonutChart).toHaveBeenCalledWith(data, theme, width, stroke); - }); -}); diff --git a/tests/render/error.test.ts b/tests/render/error.test.ts deleted file mode 100644 index 2f4d506..0000000 --- a/tests/render/error.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { describe, it, expect } from "vitest"; -import type { Theme } from "../../src/types.js"; -import { renderError } from "../../src/render/error.js"; -import { THEMES } from "../../src/constants/themes.js"; - -describe("renderError", () => { - it("renders error SVG with message", () => { - const result = renderError("Test error", 400, 300); - expect(result).toContain(` { - const theme = { bg: "#123456" } as Theme; - const result = renderError("Error", 400, 300, theme); - expect(result).toContain(`fill="#123456"`); - }); - - it("falls back to default theme when no theme provided", () => { - const result = renderError("Error", 400, 300); - expect(result).toContain(`fill="${THEMES.default.bg}"`); - }); - - it("truncates long error messages", () => { - const long = "A".repeat(50); - const result = renderError(long, 400, 300); - expect(result).toContain("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA..."); - expect(result).not.toContain("A".repeat(50)); - }); -}); diff --git a/tests/render/svg.test.ts b/tests/render/svg.test.ts deleted file mode 100644 index 3da8350..0000000 --- a/tests/render/svg.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { renderSvg } from "../../src/render/svg.js"; - -describe("renderSvg", () => { - const segments = ``; - const legend = `Legend`; - - it("renders complete SVG with all elements", () => { - const result = renderSvg(600, 400, "#ffffff", segments, legend, "Test Title", "#000000"); - - expect(result).toContain(` { - const result = renderSvg(600, 400, "#ffffff", segments, legend, null, "#000000"); - - expect(result).not.toContain(`text-anchor="middle"`); - expect(result).toContain(segments); - expect(result).toContain(legend); - }); -}); diff --git a/tests/utils/params.test.ts b/tests/utils/params.test.ts deleted file mode 100644 index 11a2db7..0000000 --- a/tests/utils/params.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { DEFAULT_CONFIG, MAX_COUNT } from "../../src/constants/config.js"; -import { THEMES } from "../../src/constants/themes.js"; -import { parseQueryParams } from "../../src/utils/params.js" - -describe("parseQueryParams", () => { - it("uses defaults when query is empty", () => { - const params = parseQueryParams({}); - - expect(params.chartType).toBe("donut"); - expect(params.chartTitle).toBe(DEFAULT_CONFIG.TITLE); - expect(params.count).toBe(DEFAULT_CONFIG.COUNT); - - expect(params.selectedTheme.bg).toBe(THEMES.default.bg); - expect(params.selectedTheme.text).toBe(THEMES.default.text); - expect(params.selectedTheme.colours).toEqual(THEMES.default.colours); - - expect(params.width).toBeGreaterThanOrEqual(DEFAULT_CONFIG.MIN_WIDTH); - expect(params.height).toBe(DEFAULT_CONFIG.HEIGHT); - expect(params.useTestData).toBe(false); - }); - - it("accepts valid chart type", () => { - const params = parseQueryParams({ type: "donut" }); - expect(params.chartType).toBe("donut"); - }) - - it("falls back to donut when type is invalid", () => { - const params = parseQueryParams({ type: "nope" }); - expect(params.chartType).toBe("donut"); - }); - - it("honours hide_title=true and blanks title", () => { - const params = parseQueryParams({ hide_title: "true", title: "Hello" }); - expect(params.chartTitle).toBe(""); - }); - - it("sanitizes title when provided", () => { - const params = parseQueryParams({ title: `alert("x")` }); - expect(params.chartTitle).toBe("<scripts>alert("x")</script>"); - }); - - it("clamps count between 1 and MAX_COUNT", () => { - expect(parseQueryParams({ count: "0" }).count).toBe(1); - expect(parseQueryParams({ count: "-5" }).count).toBe(1); - expect(parseQueryParams({ count: String(MAX_COUNT + 100) }).count).toBe(MAX_COUNT); - }); - - it("applies theme by name and allows overriding text/bg", () => { - const themeKey = Object.keys(THEMES).find((k) => k !== "default") ?? "default"; - - const params = parseQueryParams({ - theme: themeKey, - text: "#111111", - bg: "#ffffff", - }); - - expect(params.selectedTheme.text).toBe("#111111"); - expect(params.selectedTheme.bg).toBe("#ffffff"); - expect(params.selectedTheme.colours).toEqual(THEMES[themeKey as keyof typeof THEMES]!.colours); - }); - - it("if bg matches a theme name, use that theme's bg", () => { - const bgThemeKey = Object.keys(THEMES)[0]!; - const params = parseQueryParams({ bg: bgThemeKey }); - expect(params.selectedTheme.bg).toBe(THEMES[bgThemeKey as keyof typeof THEMES]!.bg); - }); - - it("overrides colours via c1..cMAX_COUNT", () => { - const params = parseQueryParams({ - c1: "#abc123", - c2: "def456", - }); - - expect(params.selectedTheme.colours[0]).toBe("#abc123"); - expect(params.selectedTheme.colours[1]).toBe("#def456"); - }); - - it("enforces MIN_WIDTH and supports explicit width/height", () => { - const small = parseQueryParams({ width: "1" }); - expect(small.width).toBe(DEFAULT_CONFIG.MIN_WIDTH); - - const exact = parseQueryParams({ width: String(DEFAULT_CONFIG.MIN_WIDTH + 10), height: "777" }); - expect(exact.width).toBe(DEFAULT_CONFIG.MIN_WIDTH + 10); - expect(exact.height).toBe(777); - }); - - it("falls back to default when count/width are non-numeric", () => { - const params = parseQueryParams({ count: "abc", width: "xyz" }); - expect(params.count).toBe(DEFAULT_CONFIG.COUNT); - expect(params.width).toBe(DEFAULT_CONFIG.WIDTH); - }) - - it("enables test mode when test=true", () => { - expect(parseQueryParams({ test: "true" }).useTestData).toBe(true); - expect(parseQueryParams({ test: "false" }).useTestData).toBe(false); - }); -}); diff --git a/tests/utils/sanitize.test.ts b/tests/utils/sanitize.test.ts deleted file mode 100644 index ad74077..0000000 --- a/tests/utils/sanitize.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { sanitize } from "../../src/utils/sanitize.js"; - -describe("sanitize", () => { - it("returns empty string for non-strings", () => { - expect(sanitize(null)).toBe(""); - expect(sanitize(undefined)).toBe(""); - expect(sanitize(123)).toBe(""); - expect(sanitize({})).toBe(""); - expect(sanitize([])).toBe(""); - }); - - it("escapes the criticial HTML characters", () => { - expect(sanitize(`<>&"'`)).toBe("<>&"'"); - }); - - it("leaves safe strings unchanged", () => { - expect(sanitize("Hello world")).toBe("Hello world"); - }); - - it("escapes mixed content correctly", () => { - expect(sanitize(`Hi & "team"`)).toBe("Hi <Mason> & "team""); - }); -}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..0d016c1 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + coverage: { + include: ["src/**", "api/**"] + } + } +});