From dc932eded7523faf4dd6de77980c47b9c0b79599 Mon Sep 17 00:00:00 2001 From: zeyus Date: Sun, 3 Nov 2024 17:33:05 +0100 Subject: [PATCH 01/11] almost done, overengineered hit counter --- node/.gitignore | 2 + node/hits.db | Bin 0 -> 8192 bytes node/hits.db-wal | Bin 0 -> 20632 bytes node/package-lock.json | 2322 +++++++++++++++++++++++++++++++++++++++ node/package.json | 26 + node/src/hit-counter.ts | 287 +++++ node/src/index.ts | 22 + 7 files changed, 2659 insertions(+) create mode 100644 node/.gitignore create mode 100644 node/hits.db create mode 100644 node/hits.db-wal create mode 100644 node/package-lock.json create mode 100644 node/package.json create mode 100644 node/src/hit-counter.ts create mode 100644 node/src/index.ts diff --git a/node/.gitignore b/node/.gitignore new file mode 100644 index 0000000..2752eb9 --- /dev/null +++ b/node/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +.DS_Store diff --git a/node/hits.db b/node/hits.db new file mode 100644 index 0000000000000000000000000000000000000000..1e4cf3000a3004295a0787ffd7822d91e09c81c0 GIT binary patch literal 8192 zcmeI#K}*9h6bJB^R2T}Q-ek82y*L=V_yw#Iq?lE$8LX#KZN-9G=(2+spDX$yX->i5j@1)Y+VbjeIavYT%Cjd1m-+qX-5{x6cIUyGsZ z5`Io@AP|561Rwwb2tWV=5P$##AOL}b6=+6ca25oj`Lx;7Tk%@j<#V0lJk(`dHNWy>v} zdVl};`S2d*i=J$1^QFwre9MQ000IagfB*srAbsP^O>@*l_I3fB*srAbfdfOxNrJmuJt{!=L*nx`17W z&ol@ifB*srAb=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", + "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", + "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", + "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", + "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", + "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", + "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", + "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", + "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", + "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", + "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", + "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", + "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", + "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", + "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", + "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", + "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", + "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", + "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", + "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", + "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", + "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", + "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", + "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.11", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.11.tgz", + "integrity": "sha512-i8KcD3PgGtGBLl3+mMYA8PdKkButvPyARxA7IQAd6qeslht13qxb1zzO8dRCtE7U3IoJS782zDBAeoKiM695kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", + "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.1.tgz", + "integrity": "sha512-CRICJIl0N5cXDONAdlTv5ShATZ4HEwk6kDDIW2/w9qOWKg+NU/5F8wYRWCrONad0/UKkloNSmmyN/wX4rtpbVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.8.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.7.tgz", + "integrity": "sha512-LidcG+2UeYIWcMuMUpBKOnryBWG/rnmOHQR5apjn8myTQcx3rinFRn7DcIFhMnS0PPFSC6OafdIKEad0lj6U0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.8" + } + }, + "node_modules/@types/qs": { + "version": "6.9.16", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz", + "integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.5.0.tgz", + "integrity": "sha512-e/6eggfOutzoK0JWiU36jsisdWoHOfN9iWiW/SieKvb7SAa6aGNmBM/UKyp+/wWSXpLlWNN8tCPwoDNPhzUvuQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", + "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.1", + "@esbuild/android-arm": "0.23.1", + "@esbuild/android-arm64": "0.23.1", + "@esbuild/android-x64": "0.23.1", + "@esbuild/darwin-arm64": "0.23.1", + "@esbuild/darwin-x64": "0.23.1", + "@esbuild/freebsd-arm64": "0.23.1", + "@esbuild/freebsd-x64": "0.23.1", + "@esbuild/linux-arm": "0.23.1", + "@esbuild/linux-arm64": "0.23.1", + "@esbuild/linux-ia32": "0.23.1", + "@esbuild/linux-loong64": "0.23.1", + "@esbuild/linux-mips64el": "0.23.1", + "@esbuild/linux-ppc64": "0.23.1", + "@esbuild/linux-riscv64": "0.23.1", + "@esbuild/linux-s390x": "0.23.1", + "@esbuild/linux-x64": "0.23.1", + "@esbuild/netbsd-x64": "0.23.1", + "@esbuild/openbsd-arm64": "0.23.1", + "@esbuild/openbsd-x64": "0.23.1", + "@esbuild/sunos-x64": "0.23.1", + "@esbuild/win32-arm64": "0.23.1", + "@esbuild/win32-ia32": "0.23.1", + "@esbuild/win32-x64": "0.23.1" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.10", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", + "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.71.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.71.0.tgz", + "integrity": "sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", + "license": "MIT" + }, + "node_modules/prebuild-install": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", + "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/tsx": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz", + "integrity": "sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.23.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/node/package.json b/node/package.json new file mode 100644 index 0000000..a4400e4 --- /dev/null +++ b/node/package.json @@ -0,0 +1,26 @@ +{ + "name": "hit_counter", + "version": "0.0.1", + "type": "module", + "description": "A simple hit counter", + "main": "dist/index.js", + "dependencies": { + "better-sqlite3": "^11.5.0", + "express": "^4.21.1", + "sharp": "^0.33.5" + }, + "scripts": { + "build": "npx tsc", + "start": "npm run build && node dist/index.js", + "dev": "npx tsx src/index.ts" + }, + "author": "zeyus", + "license": "MIT", + "devDependencies": { + "@types/better-sqlite3": "^7.6.11", + "@types/express": "^5.0.0", + "@types/node": "^22.8.7", + "tsx": "^4.19.2", + "typescript": "^5.6.3" + } +} diff --git a/node/src/hit-counter.ts b/node/src/hit-counter.ts new file mode 100644 index 0000000..72c6054 --- /dev/null +++ b/node/src/hit-counter.ts @@ -0,0 +1,287 @@ +import e, { Request, Response, NextFunction } from 'express'; +import { type Database as DB } from 'better-sqlite3'; +import { type Statement } from 'better-sqlite3'; +import Database from 'better-sqlite3'; +// import sharp for images +import sharp, { FormatEnum, AvailableFormatInfo, Sharp } from 'sharp'; +import * as fs from 'fs'; + +// Map file extensions to sharp formats +const imageFormatMap: { [key: string]: keyof FormatEnum | AvailableFormatInfo } = { + jpg: 'jpeg', + jpeg: 'jpeg', + png: 'png', + webp: 'webp', + tiff: 'tiff', + gif: 'gif' +}; + +interface HitResult { + hits: number; +} + +interface RGBColor { + r: number; + g: number; + b: number; +} + +export interface Type extends Function { new (...args: any[]): T; } + + +interface HitCounterRenderer { + render(): Promise; +} + +class BaseHitCounterRenderer implements HitCounterRenderer { + protected counter: HitCounter; + protected config: HitCounterConfig; + + constructor(counter: HitCounter, config: HitCounterConfig) { + this.counter = counter; + this.config = config; + } + + async render(): Promise { + // render the counter + throw new Error('Not implemented'); + } + +} + + +class ImageHitCounterRenderer extends BaseHitCounterRenderer { + private imageType: string; + private imageFormat: keyof FormatEnum | AvailableFormatInfo; + constructor(counter: HitCounter, config: HitCounterConfig) { + super(counter, config); + const fileExt = this.config.imageFile.split('.').pop(); + if (!fileExt) { + throw new Error('Invalid file extension'); + } + this.imageType = fileExt; + this.imageFormat = imageFormatMap[fileExt.toLowerCase()]; + if (!this.imageFormat) { + throw new Error('Unsupported file extension'); + } + } + + private async cachedBackgroundImage(overWrite: boolean = false): Promise { + // cache the image + const dest = this.config.backgroundCacheFile; + if (fs.existsSync(dest)) { + if (overWrite) { + fs.unlinkSync(dest); + } else { + return sharp(dest); + } + } + + // store the image in the cache + const fileExt = dest.split('.').pop(); + if (!fileExt) { + throw new Error('Invalid file extension'); + } + + const format = imageFormatMap[fileExt.toLowerCase()]; + if (!format) { + throw new Error('Unsupported file extension'); + } + + const image = sharp(this.config.backgroundImageUrl).toFormat(format); + await image.toFile(dest); + return image; + } + + + + async render(): Promise { + const hits = await this.counter.getHits(); + const bgImage = await this.cachedBackgroundImage(); + const bgInfo = await bgImage.metadata(); + + const text = this.config.customText; + const secondaryText = this.config.secondaryText; + const textColor = this.config.textColorRGB; + const secondaryTextColor = this.config.secondaryTextColorRGB; + const numberColor = this.config.numberColorRGB; + const frameColor = this.config.frameColorRGB; + const drawFrame = this.config.drawFrame; + const textX = this.config.textPositionX; + const textY = this.config.textPositionY; + const numberX = this.config.numberPositionX; + const numberY = this.config.numberPositionY; + + const svgFrame = ` + + + + `; + + const svgOverlay = ` + + ${text} + ${secondaryText} + ${hits} + + `; + + + + const overlay = sharp(Buffer.from(svgOverlay)); + if (drawFrame) { + const frame = sharp(Buffer.from(svgFrame)); + const composite = bgImage.composite([ + { input: await overlay.toBuffer(), gravity: 'northwest' }, + { input: await frame.toBuffer(), gravity: 'northwest' } + ]); + return composite; + } + + const composite = bgImage.composite([ + { input: await overlay.toBuffer(), gravity: 'northwest' } + ]); + + return composite; + } +} + + +interface HitCounterConfig { + siteId: number; + // path to the database file + counterDB: string; + // Background image URL for the counter display + backgroundImageUrl: string; + // local image path + imageFile: string; + backgroundCacheFile: string; + // Text settings to be displayed on the image + customText: string; // Main headline text + secondaryText: string; // Secondary descriptive text + textPositionX: number; // X position of the text + textPositionY: number; // Y position of the text + numberPositionX: number; // X position of the number + numberPositionY: number; // Y position of the number + textColorRGB: RGBColor; // Text color + secondaryTextColorRGB: RGBColor; // Secondary text color + numberColorRGB: RGBColor; // Number color + frameColorRGB: RGBColor; // Frame color + drawFrame: boolean; // Whether to draw a frame around the counter + renderer: Type; // Renderer class for the counter +} + + + +// default configuration +const defaultHitCounterConfig: HitCounterConfig = { + siteId: 1, + counterDB: './hits.db', + backgroundImageUrl: 'https://datakra.sh/assets/example_counter.jpg', + imageFile: './example_counter.png', + backgroundCacheFile: './example_counter_bg.png', + customText: 'Hit Counter!', + secondaryText: 'Super cyber, super cool!', + textPositionX: 5, + textPositionY: 5, + numberPositionX: 5, + numberPositionY: 15, + textColorRGB: { r: 253, g: 252, b: 1 }, + secondaryTextColorRGB: { r: 0, g: 255, b: 0 }, + numberColorRGB: { r: 255, g: 255, b: 255 }, + frameColorRGB: { r: 255, g: 238, b: 0 }, + drawFrame: true, + renderer: ImageHitCounterRenderer +}; + +class HitCounter { + private db: DB; + private renderer: BaseHitCounterRenderer; + private config: HitCounterConfig; + private preparedGet: Statement; + private preparedIncrement: Statement; + private preparedReset: Statement; + + constructor(renderer: Type, config?: HitCounterConfig) { + this.config = { ...defaultHitCounterConfig, ...config }; + this.renderer = new renderer(this, this.config); + this.openDB(); + this.setupDB(); + this.setupStatements(); + } + + private openDB(): void { + const db = new Database(this.config.counterDB, {fileMustExist: false}); + db.pragma('locking_mode = EXCLUSIVE'); + db.pragma('journal_mode = WAL'); + db.pragma('synchronous = OFF'); + this.db = db; + } + + private setupDB(): void { + this.db.exec(` + CREATE TABLE IF NOT EXISTS hit_counter ( + id INTEGER PRIMARY KEY, + hits INTEGER NOT NULL + ); + `); + this.db.exec(` + INSERT OR IGNORE INTO hit_counter (id, hits) + VALUES (?, ${this.config.siteId}) + `); + } + + private closeDB(): void { + this.db.close(); + } + + private setupStatements(): void { + this.preparedGet = this.db.prepare(` + SELECT hits + FROM hit_counter + WHERE id = ? + `); + this.preparedIncrement = this.db.prepare(` + UPDATE hit_counter + SET hits = hits + 1 + WHERE id = ? + `); + this.preparedReset = this.db.prepare(` + UPDATE hit_counter + SET hits = 0 + WHERE id = ? + `); + } + + async getHits(): Promise { + const result = this.preparedGet.get(this.config.siteId) as HitResult; + return result.hits; + } + + async increment(): Promise { + this.preparedIncrement.run(this.config.siteId); + } + + async reset(): Promise { + this.preparedReset.run(this.config.siteId); + } + + // destructor + destroy(): void { + this.closeDB(); + } + +} + + + + +export { + HitCounter, + HitCounterConfig, + HitCounterRenderer, + BaseHitCounterRenderer, + ImageHitCounterRenderer +}; + +export default HitCounter; diff --git a/node/src/index.ts b/node/src/index.ts new file mode 100644 index 0000000..7003aa8 --- /dev/null +++ b/node/src/index.ts @@ -0,0 +1,22 @@ +import express, { Express, Request, Response } from "express"; +import { HitCounter, ImageHitCounterRenderer } from "./hit-counter"; + +const app: Express = express(); +const PORT: number = 8000; +const hitCounter = new HitCounter(ImageHitCounterRenderer); + +app.get("/", (req: Request, res: Response) => { + res.send("Hello World!"); + hitCounter.increment(); +}); + +app.get("/hits", async (req: Request, res: Response) => { + const hits = await hitCounter.getHits(); + res.send(`This site has ${hits} hits!`); +}); + + + +app.listen(PORT, () => { + console.log(`[server]: Express server started on port ${PORT}`); +}); From e3e483e40b35015787bb56a587bf9dc797f04cfc Mon Sep 17 00:00:00 2001 From: zeyus Date: Sun, 3 Nov 2024 17:34:33 +0100 Subject: [PATCH 02/11] dont need the sqlite db. --- node/.gitignore | 2 ++ node/hits.db | Bin 8192 -> 0 bytes node/hits.db-wal | Bin 20632 -> 0 bytes 3 files changed, 2 insertions(+) delete mode 100644 node/hits.db delete mode 100644 node/hits.db-wal diff --git a/node/.gitignore b/node/.gitignore index 2752eb9..995f2b4 100644 --- a/node/.gitignore +++ b/node/.gitignore @@ -1,2 +1,4 @@ node_modules/ .DS_Store +hits.db +hits.db-* diff --git a/node/hits.db b/node/hits.db deleted file mode 100644 index 1e4cf3000a3004295a0787ffd7822d91e09c81c0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8192 zcmeI#K}*9h6bJB^R2T}Q-ek82y*L=V_yw#Iq?lE$8LX#KZN-9G=(2+spDX$yX->i5j@1)Y+VbjeIavYT%Cjd1m-+qX-5{x6cIUyGsZ z5`Io@AP|561Rwwb2tWV=5P$##AOL}b6=+6ca25oj`Lx;7Tk%@j<#V0lJk(`dHNWy>v} zdVl};`S2d*i=J$1^QFwre9MQ000IagfB*srAbsP^O>@*l_I3fB*srAbfdfOxNrJmuJt{!=L*nx`17W z&ol@ifB*srAb Date: Sun, 3 Nov 2024 21:58:43 +0100 Subject: [PATCH 03/11] WIP, pretty much there with serving image, just some layout stuff. --- node/.gitignore | 1 + node/src/hit-counter.ts | 202 ++++++++++++++++++++++++++++++---------- node/src/index.ts | 19 +++- 3 files changed, 169 insertions(+), 53 deletions(-) diff --git a/node/.gitignore b/node/.gitignore index 995f2b4..47ab89f 100644 --- a/node/.gitignore +++ b/node/.gitignore @@ -2,3 +2,4 @@ node_modules/ .DS_Store hits.db hits.db-* +example_counter_bg.png diff --git a/node/src/hit-counter.ts b/node/src/hit-counter.ts index 72c6054..d116e0c 100644 --- a/node/src/hit-counter.ts +++ b/node/src/hit-counter.ts @@ -1,10 +1,10 @@ -import e, { Request, Response, NextFunction } from 'express'; import { type Database as DB } from 'better-sqlite3'; import { type Statement } from 'better-sqlite3'; import Database from 'better-sqlite3'; // import sharp for images -import sharp, { FormatEnum, AvailableFormatInfo, Sharp } from 'sharp'; +import sharp, { FormatEnum, AvailableFormatInfo, Sharp, Metadata } from 'sharp'; import * as fs from 'fs'; +import * as https from 'https'; // Map file extensions to sharp formats const imageFormatMap: { [key: string]: keyof FormatEnum | AvailableFormatInfo } = { @@ -16,6 +16,16 @@ const imageFormatMap: { [key: string]: keyof FormatEnum | AvailableFormatInfo } gif: 'gif' }; +// no need for third party library +const mimeTypeMap: { [key: string]: string } = { + jpg: 'image/jpeg', + jpeg: 'image/jpeg', + png: 'image/png', + webp: 'image/webp', + tiff: 'image/tiff', + gif: 'image/gif' +}; + interface HitResult { hits: number; } @@ -33,6 +43,50 @@ interface HitCounterRenderer { render(): Promise; } +function fileExt(file: string): string { + const fileExt = file.split('.').pop(); + if (!fileExt) { + throw new Error('Invalid file extension'); + } + return fileExt.toLowerCase(); +} + + +function extToFormat(ext: string): keyof FormatEnum | AvailableFormatInfo { + const format = imageFormatMap[ext]; + if (!format) { + throw new Error('Unsupported file extension'); + } + return format; +} + +/** + * Determine whether the given `path` is a http(s) URL. This method + * accepts a string value and a URL instance. + * + * inspiration: + * Marcus Pöhls + * https://futurestud.io/tutorials/node-js-check-if-a-path-is-a-file-url + * + * @param {String|URL} path + * + * @returns {Boolean} + */ +function isWebURI(path) { + // `path` is neither a string nor a URL -> early return + if (typeof path !== 'string' && !(path instanceof URL)) { + return false + } + + try { + const url = new URL(path) + + return url.protocol === 'http:' || url.protocol === 'https:' + } catch (error) { + return false + } +} + class BaseHitCounterRenderer implements HitCounterRenderer { protected counter: HitCounter; protected config: HitCounterConfig; @@ -53,17 +107,31 @@ class BaseHitCounterRenderer implements HitCounterRenderer { class ImageHitCounterRenderer extends BaseHitCounterRenderer { private imageType: string; private imageFormat: keyof FormatEnum | AvailableFormatInfo; + private baseImage: Buffer; + private baseImageMetadata: Metadata; constructor(counter: HitCounter, config: HitCounterConfig) { super(counter, config); - const fileExt = this.config.imageFile.split('.').pop(); - if (!fileExt) { - throw new Error('Invalid file extension'); - } - this.imageType = fileExt; - this.imageFormat = imageFormatMap[fileExt.toLowerCase()]; - if (!this.imageFormat) { - throw new Error('Unsupported file extension'); - } + this.imageType = fileExt(this.config.imageFile); + this.imageFormat = extToFormat(this.imageType); + } + + private async downloadImage(): Promise { + return new Promise((resolve, reject) => { + let buf = Buffer.alloc(0); + const request = https.get(this.config.backgroundImageUrl, (response) => { + response.on('data', (chunk) => { + buf = Buffer.concat([buf, chunk]); + }); + response.on('end', () => { + resolve(buf); + }); + }); + request.on('error', (err) => { + fs.unlink(this.config.backgroundCacheFile, () => { + reject(err); + }); + }); + }); } private async cachedBackgroundImage(overWrite: boolean = false): Promise { @@ -78,71 +146,91 @@ class ImageHitCounterRenderer extends BaseHitCounterRenderer { } // store the image in the cache - const fileExt = dest.split('.').pop(); - if (!fileExt) { - throw new Error('Invalid file extension'); - } + const ext = fileExt(this.config.backgroundImageUrl); - const format = imageFormatMap[fileExt.toLowerCase()]; - if (!format) { - throw new Error('Unsupported file extension'); + const format = extToFormat(ext); + if (isWebURI(this.config.backgroundImageUrl)) { + const buf = await this.downloadImage(); + const image = sharp(buf).toFormat(format); + await image.toFile(dest); + return image; + } else { + if (fs.existsSync(this.config.backgroundImageUrl)) { + const image = sharp(this.config.backgroundImageUrl).toFormat(format); + await image.toFile(dest); + return image; + } else { + throw new Error('Background image does not exist'); + } } - - const image = sharp(this.config.backgroundImageUrl).toFormat(format); - await image.toFile(dest); - return image; } - - - async render(): Promise { - const hits = await this.counter.getHits(); + async prepareBaseImage(): Promise { + if (typeof this.baseImage !== 'undefined') { + return; + } const bgImage = await this.cachedBackgroundImage(); const bgInfo = await bgImage.metadata(); - - const text = this.config.customText; - const secondaryText = this.config.secondaryText; - const textColor = this.config.textColorRGB; - const secondaryTextColor = this.config.secondaryTextColorRGB; - const numberColor = this.config.numberColorRGB; - const frameColor = this.config.frameColorRGB; - const drawFrame = this.config.drawFrame; - const textX = this.config.textPositionX; - const textY = this.config.textPositionY; - const numberX = this.config.numberPositionX; - const numberY = this.config.numberPositionY; + const imgWidth = bgInfo.width; + const imgHeight = bgInfo.height; + if (typeof imgWidth === 'undefined' || typeof imgHeight === 'undefined') { + throw new Error('Invalid image dimensions'); + } const svgFrame = ` - + `; const svgOverlay = ` - ${text} - ${secondaryText} - ${hits} + ${this.config.customText} + ${this.config.secondaryText} `; - - const overlay = sharp(Buffer.from(svgOverlay)); - if (drawFrame) { + + if (this.config.drawFrame) { const frame = sharp(Buffer.from(svgFrame)); const composite = bgImage.composite([ { input: await overlay.toBuffer(), gravity: 'northwest' }, { input: await frame.toBuffer(), gravity: 'northwest' } ]); - return composite; + const preparedImg = composite.toFormat(this.imageFormat); + this.baseImageMetadata = await preparedImg.metadata(); + this.baseImage = await preparedImg.toBuffer();; } const composite = bgImage.composite([ { input: await overlay.toBuffer(), gravity: 'northwest' } ]); - return composite; + const preparedImg = composite.toFormat(this.imageFormat); + this.baseImageMetadata = await preparedImg.metadata(); + this.baseImage = await preparedImg.toBuffer(); + + } + + async render(): Promise { + const hits = await this.counter.getHits(); + + const numberColor = this.config.numberColorRGB; + const numberX = this.config.numberPositionX; + const numberY = this.config.numberPositionY; + + await this.prepareBaseImage(); + const overlay = ` + + ${hits} + + `; + const composite = sharp(this.baseImage).composite([ + { input: Buffer.from(overlay), gravity: 'northwest' } + ]); + return composite.toBuffer(); + } } @@ -163,6 +251,9 @@ interface HitCounterConfig { textPositionY: number; // Y position of the text numberPositionX: number; // X position of the number numberPositionY: number; // Y position of the number + fontSize: number; // Font size + fontFace: string; // Font face + borderWidth: number; // Border width textColorRGB: RGBColor; // Text color secondaryTextColorRGB: RGBColor; // Secondary text color numberColorRGB: RGBColor; // Number color @@ -184,12 +275,15 @@ const defaultHitCounterConfig: HitCounterConfig = { secondaryText: 'Super cyber, super cool!', textPositionX: 5, textPositionY: 5, - numberPositionX: 5, + numberPositionX: 160, numberPositionY: 15, + fontSize: 12, + fontFace: 'Arial', + borderWidth: 3, textColorRGB: { r: 253, g: 252, b: 1 }, secondaryTextColorRGB: { r: 0, g: 255, b: 0 }, numberColorRGB: { r: 255, g: 255, b: 255 }, - frameColorRGB: { r: 255, g: 238, b: 0 }, + frameColorRGB: { r: 255, g: 0, b: 0 }, drawFrame: true, renderer: ImageHitCounterRenderer }; @@ -201,6 +295,7 @@ class HitCounter { private preparedGet: Statement; private preparedIncrement: Statement; private preparedReset: Statement; + private mimeType: string; constructor(renderer: Type, config?: HitCounterConfig) { this.config = { ...defaultHitCounterConfig, ...config }; @@ -208,6 +303,7 @@ class HitCounter { this.openDB(); this.setupDB(); this.setupStatements(); + this.mimeType = mimeTypeMap[fileExt(this.config.imageFile)]; } private openDB(): void { @@ -266,6 +362,14 @@ class HitCounter { this.preparedReset.run(this.config.siteId); } + async render(): Promise { + return this.renderer.render(); + } + + getMimeType(): string { + return this.mimeType; + } + // destructor destroy(): void { this.closeDB(); diff --git a/node/src/index.ts b/node/src/index.ts index 7003aa8..c3647a6 100644 --- a/node/src/index.ts +++ b/node/src/index.ts @@ -4,18 +4,29 @@ import { HitCounter, ImageHitCounterRenderer } from "./hit-counter"; const app: Express = express(); const PORT: number = 8000; const hitCounter = new HitCounter(ImageHitCounterRenderer); +const imageMimeType = hitCounter.getMimeType(); app.get("/", (req: Request, res: Response) => { - res.send("Hello World!"); - hitCounter.increment(); + hitCounter.increment(); + res.send("Hello dear interlocutor! Your visit has been noted."); + }); app.get("/hits", async (req: Request, res: Response) => { - const hits = await hitCounter.getHits(); - res.send(`This site has ${hits} hits!`); + const hits = await hitCounter.getHits(); + res.send(`This site has ${hits} hits!`); }); +app.get("/reset", async (req: Request, res: Response) => { + await hitCounter.reset(); + res.send("Hit counter has been reset!"); +}); +app.get("/image", async (req: Request, res: Response) => { + const image = await hitCounter.render(); + res.setHeader("Content-Type", imageMimeType); + res.send(image); +}); app.listen(PORT, () => { console.log(`[server]: Express server started on port ${PORT}`); From b08f2594471031a1c19989624d9a78bd85bcb658 Mon Sep 17 00:00:00 2001 From: zeyus Date: Wed, 6 Nov 2024 17:21:05 +0100 Subject: [PATCH 04/11] Done and done. --- node/nodemon.json | 6 + node/package-lock.json | 350 +++++++++++++++++++++++++++++++++++++++- node/package.json | 5 +- node/src/hit-counter.ts | 79 +++++---- 4 files changed, 402 insertions(+), 38 deletions(-) create mode 100644 node/nodemon.json diff --git a/node/nodemon.json b/node/nodemon.json new file mode 100644 index 0000000..4acba0f --- /dev/null +++ b/node/nodemon.json @@ -0,0 +1,6 @@ +{ + "watch": ["src"], + "ext": "ts,json", + "ignore": ["src/**/*.spec.ts"], + "exec": "npx tsx ./src/index.ts" +} diff --git a/node/package-lock.json b/node/package-lock.json index bec5fe7..8f4ecee 100644 --- a/node/package-lock.json +++ b/node/package-lock.json @@ -1,12 +1,12 @@ { "name": "hit_counter", - "version": "0.0.1", + "version": "0.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "hit_counter", - "version": "0.0.1", + "version": "0.0.2", "license": "MIT", "dependencies": { "better-sqlite3": "^11.5.0", @@ -17,6 +17,7 @@ "@types/better-sqlite3": "^7.6.11", "@types/express": "^5.0.0", "@types/node": "^22.8.7", + "nodemon": "^3.1.7", "tsx": "^4.19.2", "typescript": "^5.6.3" } @@ -931,12 +932,33 @@ "node": ">= 0.6" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -968,6 +990,19 @@ "prebuild-install": "^7.1.1" } }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bindings": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", @@ -1012,6 +1047,30 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -1064,6 +1123,31 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", @@ -1111,6 +1195,13 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -1382,6 +1473,19 @@ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", "license": "MIT" }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/finalhandler": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", @@ -1486,6 +1590,19 @@ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", "license": "MIT" }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -1498,6 +1615,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/has-property-descriptors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", @@ -1594,6 +1721,13 @@ ], "license": "BSD-3-Clause" }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1621,6 +1755,52 @@ "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", "license": "MIT" }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -1693,6 +1873,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", @@ -1741,6 +1934,70 @@ "node": ">=10" } }, + "node_modules/nodemon": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz", + "integrity": "sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/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, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", @@ -1789,6 +2046,19 @@ "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", "license": "MIT" }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/prebuild-install": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", @@ -1828,6 +2098,13 @@ "node": ">= 0.10" } }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, "node_modules/pump": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", @@ -1906,6 +2183,19 @@ "node": ">= 6" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -2142,6 +2432,19 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -2169,6 +2472,19 @@ "node": ">=0.10.0" } }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/tar-fs": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", @@ -2197,6 +2513,19 @@ "node": ">=6" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -2206,6 +2535,16 @@ "node": ">=0.6" } }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -2272,6 +2611,13 @@ "node": ">=14.17" } }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", diff --git a/node/package.json b/node/package.json index a4400e4..d482a05 100644 --- a/node/package.json +++ b/node/package.json @@ -1,6 +1,6 @@ { "name": "hit_counter", - "version": "0.0.1", + "version": "0.0.2", "type": "module", "description": "A simple hit counter", "main": "dist/index.js", @@ -12,7 +12,7 @@ "scripts": { "build": "npx tsc", "start": "npm run build && node dist/index.js", - "dev": "npx tsx src/index.ts" + "dev": "npx nodemon" }, "author": "zeyus", "license": "MIT", @@ -20,6 +20,7 @@ "@types/better-sqlite3": "^7.6.11", "@types/express": "^5.0.0", "@types/node": "^22.8.7", + "nodemon": "^3.1.7", "tsx": "^4.19.2", "typescript": "^5.6.3" } diff --git a/node/src/hit-counter.ts b/node/src/hit-counter.ts index d116e0c..63ab321 100644 --- a/node/src/hit-counter.ts +++ b/node/src/hit-counter.ts @@ -2,7 +2,7 @@ import { type Database as DB } from 'better-sqlite3'; import { type Statement } from 'better-sqlite3'; import Database from 'better-sqlite3'; // import sharp for images -import sharp, { FormatEnum, AvailableFormatInfo, Sharp, Metadata } from 'sharp'; +import sharp, { FormatEnum, AvailableFormatInfo, Sharp, Metadata, TextAlign } from 'sharp'; import * as fs from 'fs'; import * as https from 'https'; @@ -173,41 +173,38 @@ class ImageHitCounterRenderer extends BaseHitCounterRenderer { const bgInfo = await bgImage.metadata(); const imgWidth = bgInfo.width; const imgHeight = bgInfo.height; - if (typeof imgWidth === 'undefined' || typeof imgHeight === 'undefined') { + const imgDensity = bgInfo.density; + if (typeof imgWidth === 'undefined' || typeof imgHeight === 'undefined' || typeof imgDensity === 'undefined') { throw new Error('Invalid image dimensions'); } - const svgFrame = ` - - - - `; + let svgFrame = ''; + if (this.config.drawFrame) { + svgFrame = ``; + + } const svgOverlay = ` - - ${this.config.customText} - ${this.config.secondaryText} + + + ${this.config.customText} + ${this.config.secondaryText} + + ${svgFrame} `; - const overlay = sharp(Buffer.from(svgOverlay)); - - if (this.config.drawFrame) { - const frame = sharp(Buffer.from(svgFrame)); - const composite = bgImage.composite([ - { input: await overlay.toBuffer(), gravity: 'northwest' }, - { input: await frame.toBuffer(), gravity: 'northwest' } - ]); - const preparedImg = composite.toFormat(this.imageFormat); - this.baseImageMetadata = await preparedImg.metadata(); - this.baseImage = await preparedImg.toBuffer();; - } + const overlay = Buffer.from(svgOverlay); const composite = bgImage.composite([ - { input: await overlay.toBuffer(), gravity: 'northwest' } + { + input: overlay, + top: 0, + left: 0 + } ]); - const preparedImg = composite.toFormat(this.imageFormat); + const preparedImg = composite.toFormat(this.imageFormat).withMetadata(); this.baseImageMetadata = await preparedImg.metadata(); this.baseImage = await preparedImg.toBuffer(); @@ -216,19 +213,33 @@ class ImageHitCounterRenderer extends BaseHitCounterRenderer { async render(): Promise { const hits = await this.counter.getHits(); - const numberColor = this.config.numberColorRGB; const numberX = this.config.numberPositionX; const numberY = this.config.numberPositionY; await this.prepareBaseImage(); - const overlay = ` - - ${hits} + + const imgWidth = this.baseImageMetadata.width; + const imgHeight = this.baseImageMetadata.height; + if (typeof imgWidth === 'undefined' || typeof imgHeight === 'undefined') { + throw new Error('Invalid image dimensions'); + } + + const svgNumber = ` + + ${hits} `; + + const numberOverlay = Buffer.from(svgNumber); + const composite = sharp(this.baseImage).composite([ - { input: Buffer.from(overlay), gravity: 'northwest' } + { + input: numberOverlay, + top: 0, + left: 0 + } ]); + return composite.toBuffer(); } @@ -274,11 +285,11 @@ const defaultHitCounterConfig: HitCounterConfig = { customText: 'Hit Counter!', secondaryText: 'Super cyber, super cool!', textPositionX: 5, - textPositionY: 5, - numberPositionX: 160, - numberPositionY: 15, - fontSize: 12, - fontFace: 'Arial', + textPositionY: 11, + numberPositionX: 170, + numberPositionY: 18, + fontSize: 10, + fontFace: 'fixed', borderWidth: 3, textColorRGB: { r: 253, g: 252, b: 1 }, secondaryTextColorRGB: { r: 0, g: 255, b: 0 }, From 654eaa5c5964370922099314f04665cdc6ce14d0 Mon Sep 17 00:00:00 2001 From: zeyus Date: Wed, 6 Nov 2024 17:33:17 +0100 Subject: [PATCH 05/11] README --- node/README.md | 147 ++++++++++++++++++++++++++++++++++++++++ node/src/hit-counter.ts | 2 - 2 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 node/README.md diff --git a/node/README.md b/node/README.md new file mode 100644 index 0000000..e017787 --- /dev/null +++ b/node/README.md @@ -0,0 +1,147 @@ +# Hit Counter - Because Nothing Says Retro Like a Hit Counter + +[Remember Hit Counters? Well, They’re Back. And They’re Gloriously useless](https://datakra.sh/logs/remember-hit-counters-well-theyre-back-and-theyre-gloriously-useless), personal websites were as unique as fingerprints, and every site proudly flaunted a hit counter? It was the ultimate digital badge of honor—a glaring declaration of how many lost souls had wandered onto your site. Fast forward to now, and it turns out you can relive those glitzy days. Yup, hit counters are back, and they are as gloriously pointless as ever! + +## Why, Though? + +Why on Satans earth would anyone need a hit counter in 2024? Honestly, I haven't got the slightest idea. Most modern web metrics tools can provide far more detailed insights. But hey, who says everything needs to make sense? Sometimes, a little nostalgia is all you need to spice things up. + +## Features + +- **Custom Main Text and Secondary Text**: Because why settle for one when you can have two? +- **Hit Counter**: See how many times the page is hit. It’s like a pissing contest, but for web traffic. +- **Frame Option**: For when you want to make things a tad more "extra." +- **Custom Colors**: Match your counter to your site's funky style. +- **Custom Output**: jpg, png, webp, tiff, gif. High-tech stuff. + +## Requirements + +- Node.js 22.x or higher + +## Installation + +1. **Clone this fantastic repository**: + + ```sh + git clone https://git.cyberwa.re/revengeday/hit-counter.git + ``` + +2. **Navigate** to the project directory: + + ```sh + cd hit-counter/node + ``` + +3. **Install**: + + ```sh + npm i + ``` + +4. **Run devserver** in your PHP configuration (`php.ini`): + + ```sh + npm run dev + ``` + +5. **Enjoy the Retro Vibes**: Access the script from your web browser or via `curl`: + + ```sh + http://localhost:8000/ + http://localhost:8000/hits # to see the count + http://localhost:8000/image # to see the image + http://localhost:8000/reset # to reset the count + ``` + +## Configuration + +```ts +// the config is defined in the HitCounterConfig interface +interface HitCounterConfig { + siteId: number; + // path to the database file + counterDB: string; + // Background image URL for the counter display + backgroundImageUrl: string; + // local image path + imageFile: string; + backgroundCacheFile: string; + // Text settings to be displayed on the image + customText: string; // Main headline text + secondaryText: string; // Secondary descriptive text + textPositionX: number; // X position of the text + textPositionY: number; // Y position of the text + numberPositionX: number; // X position of the number + numberPositionY: number; // Y position of the number + fontSize: number; // Font size + fontFace: string; // Font face + borderWidth: number; // Border width + textColorRGB: RGBColor; // Text color + secondaryTextColorRGB: RGBColor; // Secondary text color + numberColorRGB: RGBColor; // Number color + frameColorRGB: RGBColor; // Frame color + drawFrame: boolean; // Whether to draw a frame around the counter +} + +// currently only the ImageHitCounterRenderer is implemented, but this could be extended +// to a canvas or html or whatever renderer +``` + +## Usage + +```ts +import { HitCounter, ImageHitCounterRenderer } from "./hit-counter"; + +defaultHitCounterConfig = { + siteId: 1, + counterDB: './hits.db', + backgroundImageUrl: 'https://datakra.sh/assets/example_counter.jpg', + imageFile: './example_counter.png', + backgroundCacheFile: './example_counter_bg.png', + customText: 'Hit Counter!', + secondaryText: 'Super cyber, super cool!', + textPositionX: 5, + textPositionY: 11, + numberPositionX: 170, + numberPositionY: 18, + fontSize: 10, + fontFace: 'fixed', + borderWidth: 3, + textColorRGB: { r: 253, g: 252, b: 1 }, + secondaryTextColorRGB: { r: 0, g: 255, b: 0 }, + numberColorRGB: { r: 255, g: 255, b: 255 }, + frameColorRGB: { r: 255, g: 0, b: 0 }, + drawFrame: true, + renderer: ImageHitCounterRenderer +}; + +const hitCounter = new HitCounter(ImageHitCounterRenderer, defaultHitCounterConfig); + +// increment the hit counter +hitCounter.increment(); // async function + +// get the current hit count +const count = await hitCounter.getCount(); + +// get mime type +const imageMimeType = hitCounter.getMimeType(); + +// render the image +const image = await hitCounter.render(); + +// send to client however you like + +// reset the hit counter +await hitCounter.reset(); +``` + + + + +## Contributing + +Want to make this even more useless? Fork it, hack it, and send me a pull request. + +## Contact + +Got questions or just feeling nostalgic? Hit me up on the fediverse: [@revengeday@corteximplant.com](https://corteximplant.com/@revengeday). diff --git a/node/src/hit-counter.ts b/node/src/hit-counter.ts index 63ab321..dc419b4 100644 --- a/node/src/hit-counter.ts +++ b/node/src/hit-counter.ts @@ -270,7 +270,6 @@ interface HitCounterConfig { numberColorRGB: RGBColor; // Number color frameColorRGB: RGBColor; // Frame color drawFrame: boolean; // Whether to draw a frame around the counter - renderer: Type; // Renderer class for the counter } @@ -296,7 +295,6 @@ const defaultHitCounterConfig: HitCounterConfig = { numberColorRGB: { r: 255, g: 255, b: 255 }, frameColorRGB: { r: 255, g: 0, b: 0 }, drawFrame: true, - renderer: ImageHitCounterRenderer }; class HitCounter { From b6f83af72e35b40896c62b9e88f2d2c844d392c7 Mon Sep 17 00:00:00 2001 From: zeyus Date: Thu, 7 Nov 2024 11:21:25 +0000 Subject: [PATCH 06/11] PHPN'T --- node/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/README.md b/node/README.md index e017787..8160a2a 100644 --- a/node/README.md +++ b/node/README.md @@ -38,7 +38,7 @@ Why on Satans earth would anyone need a hit counter in 2024? Honestly, I haven't npm i ``` -4. **Run devserver** in your PHP configuration (`php.ini`): +4. **Run devserver** to test it out locally: ```sh npm run dev From 0c744f0883a952d999647c3d97a870d7e9844c0a Mon Sep 17 00:00:00 2001 From: zeyus Date: Thu, 7 Nov 2024 13:35:00 +0100 Subject: [PATCH 07/11] Made it a bit nicer, fixed a bug with the initial hit count. --- node/README.md | 6 +- node/src/hit-counter.ts | 432 ++++++++++++++++------------------------ 2 files changed, 172 insertions(+), 266 deletions(-) diff --git a/node/README.md b/node/README.md index 8160a2a..d81af7a 100644 --- a/node/README.md +++ b/node/README.md @@ -92,6 +92,8 @@ interface HitCounterConfig { ```ts import { HitCounter, ImageHitCounterRenderer } from "./hit-counter"; + +// default config defaultHitCounterConfig = { siteId: 1, counterDB: './hits.db', @@ -115,7 +117,9 @@ defaultHitCounterConfig = { renderer: ImageHitCounterRenderer }; -const hitCounter = new HitCounter(ImageHitCounterRenderer, defaultHitCounterConfig); +// the default config will be used, but you can pass a custom config object as +// the second argument to the HitCounter constructor +const hitCounter = new HitCounter(ImageHitCounterRenderer); // increment the hit counter hitCounter.increment(); // async function diff --git a/node/src/hit-counter.ts b/node/src/hit-counter.ts index dc419b4..a148ccc 100644 --- a/node/src/hit-counter.ts +++ b/node/src/hit-counter.ts @@ -1,13 +1,9 @@ -import { type Database as DB } from 'better-sqlite3'; -import { type Statement } from 'better-sqlite3'; -import Database from 'better-sqlite3'; -// import sharp for images -import sharp, { FormatEnum, AvailableFormatInfo, Sharp, Metadata, TextAlign } from 'sharp'; +import Database, { Database as DB, Statement } from 'better-sqlite3'; +import sharp, { FormatEnum, Metadata, Sharp } from 'sharp'; import * as fs from 'fs'; import * as https from 'https'; -// Map file extensions to sharp formats -const imageFormatMap: { [key: string]: keyof FormatEnum | AvailableFormatInfo } = { +const imageFormatMap: Record = { jpg: 'jpeg', jpeg: 'jpeg', png: 'png', @@ -16,8 +12,7 @@ const imageFormatMap: { [key: string]: keyof FormatEnum | AvailableFormatInfo } gif: 'gif' }; -// no need for third party library -const mimeTypeMap: { [key: string]: string } = { +const mimeTypeMap: Record = { jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', @@ -38,214 +33,10 @@ interface RGBColor { export interface Type extends Function { new (...args: any[]): T; } - interface HitCounterRenderer { render(): Promise; } -function fileExt(file: string): string { - const fileExt = file.split('.').pop(); - if (!fileExt) { - throw new Error('Invalid file extension'); - } - return fileExt.toLowerCase(); -} - - -function extToFormat(ext: string): keyof FormatEnum | AvailableFormatInfo { - const format = imageFormatMap[ext]; - if (!format) { - throw new Error('Unsupported file extension'); - } - return format; -} - -/** - * Determine whether the given `path` is a http(s) URL. This method - * accepts a string value and a URL instance. - * - * inspiration: - * Marcus Pöhls - * https://futurestud.io/tutorials/node-js-check-if-a-path-is-a-file-url - * - * @param {String|URL} path - * - * @returns {Boolean} - */ -function isWebURI(path) { - // `path` is neither a string nor a URL -> early return - if (typeof path !== 'string' && !(path instanceof URL)) { - return false - } - - try { - const url = new URL(path) - - return url.protocol === 'http:' || url.protocol === 'https:' - } catch (error) { - return false - } -} - -class BaseHitCounterRenderer implements HitCounterRenderer { - protected counter: HitCounter; - protected config: HitCounterConfig; - - constructor(counter: HitCounter, config: HitCounterConfig) { - this.counter = counter; - this.config = config; - } - - async render(): Promise { - // render the counter - throw new Error('Not implemented'); - } - -} - - -class ImageHitCounterRenderer extends BaseHitCounterRenderer { - private imageType: string; - private imageFormat: keyof FormatEnum | AvailableFormatInfo; - private baseImage: Buffer; - private baseImageMetadata: Metadata; - constructor(counter: HitCounter, config: HitCounterConfig) { - super(counter, config); - this.imageType = fileExt(this.config.imageFile); - this.imageFormat = extToFormat(this.imageType); - } - - private async downloadImage(): Promise { - return new Promise((resolve, reject) => { - let buf = Buffer.alloc(0); - const request = https.get(this.config.backgroundImageUrl, (response) => { - response.on('data', (chunk) => { - buf = Buffer.concat([buf, chunk]); - }); - response.on('end', () => { - resolve(buf); - }); - }); - request.on('error', (err) => { - fs.unlink(this.config.backgroundCacheFile, () => { - reject(err); - }); - }); - }); - } - - private async cachedBackgroundImage(overWrite: boolean = false): Promise { - // cache the image - const dest = this.config.backgroundCacheFile; - if (fs.existsSync(dest)) { - if (overWrite) { - fs.unlinkSync(dest); - } else { - return sharp(dest); - } - } - - // store the image in the cache - const ext = fileExt(this.config.backgroundImageUrl); - - const format = extToFormat(ext); - if (isWebURI(this.config.backgroundImageUrl)) { - const buf = await this.downloadImage(); - const image = sharp(buf).toFormat(format); - await image.toFile(dest); - return image; - } else { - if (fs.existsSync(this.config.backgroundImageUrl)) { - const image = sharp(this.config.backgroundImageUrl).toFormat(format); - await image.toFile(dest); - return image; - } else { - throw new Error('Background image does not exist'); - } - } - } - - async prepareBaseImage(): Promise { - if (typeof this.baseImage !== 'undefined') { - return; - } - const bgImage = await this.cachedBackgroundImage(); - const bgInfo = await bgImage.metadata(); - const imgWidth = bgInfo.width; - const imgHeight = bgInfo.height; - const imgDensity = bgInfo.density; - if (typeof imgWidth === 'undefined' || typeof imgHeight === 'undefined' || typeof imgDensity === 'undefined') { - throw new Error('Invalid image dimensions'); - } - - let svgFrame = ''; - if (this.config.drawFrame) { - svgFrame = ``; - - } - - const svgOverlay = ` - - - ${this.config.customText} - ${this.config.secondaryText} - - ${svgFrame} - - `; - - const overlay = Buffer.from(svgOverlay); - - const composite = bgImage.composite([ - { - input: overlay, - top: 0, - left: 0 - } - ]); - - const preparedImg = composite.toFormat(this.imageFormat).withMetadata(); - this.baseImageMetadata = await preparedImg.metadata(); - this.baseImage = await preparedImg.toBuffer(); - - } - - async render(): Promise { - const hits = await this.counter.getHits(); - - const numberX = this.config.numberPositionX; - const numberY = this.config.numberPositionY; - - await this.prepareBaseImage(); - - const imgWidth = this.baseImageMetadata.width; - const imgHeight = this.baseImageMetadata.height; - if (typeof imgWidth === 'undefined' || typeof imgHeight === 'undefined') { - throw new Error('Invalid image dimensions'); - } - - const svgNumber = ` - - ${hits} - - `; - - const numberOverlay = Buffer.from(svgNumber); - - const composite = sharp(this.baseImage).composite([ - { - input: numberOverlay, - top: 0, - left: 0 - } - ]); - - return composite.toBuffer(); - - } -} - - interface HitCounterConfig { siteId: number; // path to the database file @@ -272,9 +63,6 @@ interface HitCounterConfig { drawFrame: boolean; // Whether to draw a frame around the counter } - - -// default configuration const defaultHitCounterConfig: HitCounterConfig = { siteId: 1, counterDB: './hits.db', @@ -297,78 +85,198 @@ const defaultHitCounterConfig: HitCounterConfig = { drawFrame: true, }; +function fileExt(file: string): string { + const ext = file.split('.').pop(); + if (!ext) throw new Error('Invalid file extension'); + return ext.toLowerCase(); +} + +function extToFormat(ext: string): keyof FormatEnum { + const format = imageFormatMap[ext]; + if (!format) throw new Error('Unsupported file extension'); + return format; +} + +/** + * Determine whether the given `path` is a http(s) URL. This method + * accepts a string value and a URL instance. + * + * inspiration: + * Marcus Pöhls + * https://futurestud.io/tutorials/node-js-check-if-a-path-is-a-file-url + * + * @param {String|URL} path + * + * @returns {Boolean} + */ +function isWebURI(path: string | URL): boolean { + try { + const url = new URL(path.toString()); + return url.protocol === 'http:' || url.protocol === 'https:'; + } catch { + return false; + } +} + +class BaseHitCounterRenderer implements HitCounterRenderer { + protected counter: HitCounter; + protected config: HitCounterConfig; + + constructor(counter: HitCounter, config: HitCounterConfig) { + this.counter = counter; + this.config = config; + } + + async render(): Promise { + throw new Error('Render not implemented'); + } +} + +class ImageHitCounterRenderer extends BaseHitCounterRenderer { + private imageFormat: keyof FormatEnum; + private baseImage: Buffer; + private baseImageMetadata: Metadata; + + constructor(counter: HitCounter, config: HitCounterConfig) { + super(counter, config); + this.imageFormat = extToFormat(fileExt(this.config.imageFile)); + } + + private async downloadImage(): Promise { + return new Promise((resolve, reject) => { + let buf = Buffer.alloc(0); + https.get(this.config.backgroundImageUrl, (response) => { + response.on('data', (chunk) => (buf = Buffer.concat([buf, chunk]))); + response.on('end', () => resolve(buf)); + }).on('error', reject); + }); + } + + private async loadOrCacheBG(overwrite = false): Promise { + if (fs.existsSync(this.config.backgroundCacheFile) && !overwrite) { + return sharp(this.config.backgroundCacheFile); + } + + const imageBuffer = isWebURI(this.config.backgroundImageUrl) + ? await this.downloadImage() + : fs.readFileSync(this.config.backgroundImageUrl); + + const image = sharp(imageBuffer).toFormat(this.imageFormat); + await image.toFile(this.config.backgroundCacheFile); + return image; + } + + private async getBaseImage(): Promise { + if (typeof this.baseImage !== 'undefined') { + return this.baseImage; + } + const bgImage = await this.loadOrCacheBG(); + const bgMetadata = await bgImage.metadata(); + + const svgOverlay = this.createSvgOverlay(bgMetadata.width!, bgMetadata.height!); + const compositeImage = bgImage.composite([{ input: svgOverlay, top: 0, left: 0, density: bgMetadata.density! }]).withMetadata(); + this.baseImage = await compositeImage.toBuffer(); + this.baseImageMetadata = await compositeImage.metadata(); + + return this.baseImage; + } + + private createSvgOverlay(width: number, height: number): Buffer { + const { textPositionX, textPositionY, fontFace, fontSize, customText, secondaryText, frameColorRGB, drawFrame } = this.config; + const frame = drawFrame + ? `` + : ''; + + const svg = ` + + + ${customText} + ${secondaryText} + + ${frame} + `; + return Buffer.from(svg); + } + + private createSVGNumberOverlay(hits: number, imgWidth: number, imgHeight: number): Buffer { + const { numberPositionX, numberPositionY, fontFace, fontSize, numberColorRGB } = this.config; + const svg = ` + + ${hits} + `; + return Buffer.from(svg); + } + + async render(): Promise { + const hits = await this.counter.getHits(); + return await sharp(await this.getBaseImage()) + .composite([ + { + input: this.createSVGNumberOverlay(hits, this.baseImageMetadata.width!, this.baseImageMetadata.height!), + top: 0, + left: 0 + } + ]) + .toFormat(this.imageFormat) + .toBuffer(); + } +} + class HitCounter { private db: DB; private renderer: BaseHitCounterRenderer; - private config: HitCounterConfig; - private preparedGet: Statement; - private preparedIncrement: Statement; - private preparedReset: Statement; + private preparedStatements: Record; private mimeType: string; + private config: HitCounterConfig; - constructor(renderer: Type, config?: HitCounterConfig) { - this.config = { ...defaultHitCounterConfig, ...config }; - this.renderer = new renderer(this, this.config); - this.openDB(); - this.setupDB(); - this.setupStatements(); - this.mimeType = mimeTypeMap[fileExt(this.config.imageFile)]; + constructor(renderer: Type, config: HitCounterConfig = defaultHitCounterConfig) { + config = { ...defaultHitCounterConfig, ...config }; + this.config = config; + this.renderer = new renderer(this, config); + this.mimeType = mimeTypeMap[fileExt(config.imageFile)]; + this.db = this.initDB(config.counterDB); + this.preparedStatements = this.prepareStatements(); } - private openDB(): void { - const db = new Database(this.config.counterDB, {fileMustExist: false}); + private initDB(filePath: string): DB { + const db = new Database(filePath, {fileMustExist: false}); db.pragma('locking_mode = EXCLUSIVE'); db.pragma('journal_mode = WAL'); db.pragma('synchronous = OFF'); - this.db = db; + return db; } - private setupDB(): void { + private prepareStatements(): Record { this.db.exec(` CREATE TABLE IF NOT EXISTS hit_counter ( id INTEGER PRIMARY KEY, - hits INTEGER NOT NULL + hits INTEGER NOT NULL DEFAULT 0 ); `); + this.db.exec(` INSERT OR IGNORE INTO hit_counter (id, hits) - VALUES (?, ${this.config.siteId}) + VALUES (${this.config.siteId}, 0) `); - } - private closeDB(): void { - this.db.close(); - } - - private setupStatements(): void { - this.preparedGet = this.db.prepare(` - SELECT hits - FROM hit_counter - WHERE id = ? - `); - this.preparedIncrement = this.db.prepare(` - UPDATE hit_counter - SET hits = hits + 1 - WHERE id = ? - `); - this.preparedReset = this.db.prepare(` - UPDATE hit_counter - SET hits = 0 - WHERE id = ? - `); + return { + get: this.db.prepare('SELECT hits FROM hit_counter WHERE id = ?'), + increment: this.db.prepare('UPDATE hit_counter SET hits = hits + 1 WHERE id = ?'), + reset: this.db.prepare('UPDATE hit_counter SET hits = 0 WHERE id = ?'), + }; } async getHits(): Promise { - const result = this.preparedGet.get(this.config.siteId) as HitResult; + const result = this.preparedStatements.get.get(this.config.siteId) as HitResult; return result.hits; } async increment(): Promise { - this.preparedIncrement.run(this.config.siteId); + this.preparedStatements.increment.run(this.config.siteId); } async reset(): Promise { - this.preparedReset.run(this.config.siteId); + this.preparedStatements.reset.run(this.config.siteId); } async render(): Promise { @@ -379,16 +287,11 @@ class HitCounter { return this.mimeType; } - // destructor - destroy(): void { - this.closeDB(); + finalize(): void { + this.db.close(); } - } - - - export { HitCounter, HitCounterConfig, @@ -396,5 +299,4 @@ export { BaseHitCounterRenderer, ImageHitCounterRenderer }; - export default HitCounter; From 51cd15aa4ffc008553b89a596d3918182c9f6311 Mon Sep 17 00:00:00 2001 From: zeyus Date: Thu, 7 Nov 2024 14:03:03 +0100 Subject: [PATCH 08/11] Comments! --- node/src/hit-counter.ts | 168 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 166 insertions(+), 2 deletions(-) diff --git a/node/src/hit-counter.ts b/node/src/hit-counter.ts index a148ccc..c6a5d5c 100644 --- a/node/src/hit-counter.ts +++ b/node/src/hit-counter.ts @@ -3,6 +3,7 @@ import sharp, { FormatEnum, Metadata, Sharp } from 'sharp'; import * as fs from 'fs'; import * as https from 'https'; +// Define the image format map const imageFormatMap: Record = { jpg: 'jpeg', jpeg: 'jpeg', @@ -12,6 +13,7 @@ const imageFormatMap: Record = { gif: 'gif' }; +// Define the MIME type map const mimeTypeMap: Record = { jpg: 'image/jpeg', jpeg: 'image/jpeg', @@ -21,22 +23,27 @@ const mimeTypeMap: Record = { gif: 'image/gif' }; +// Database result for hits interface HitResult { hits: number; } +// RGB color type interface RGBColor { r: number; g: number; b: number; } +// Allows use of base class and subclass as a valid type export interface Type extends Function { new (...args: any[]): T; } +// Hit counter renderer interface interface HitCounterRenderer { render(): Promise; } +// Hit counter configuration interface HitCounterConfig { siteId: number; // path to the database file @@ -63,6 +70,7 @@ interface HitCounterConfig { drawFrame: boolean; // Whether to draw a frame around the counter } +// Default configuration for the hit counter const defaultHitCounterConfig: HitCounterConfig = { siteId: 1, counterDB: './hits.db', @@ -85,12 +93,29 @@ const defaultHitCounterConfig: HitCounterConfig = { drawFrame: true, }; + +/** + * Extract the file extension from a file path. + * + * @param {String} file + * + * @returns {String} + * @throws {Error} if the file extension is invalid + */ function fileExt(file: string): string { const ext = file.split('.').pop(); if (!ext) throw new Error('Invalid file extension'); return ext.toLowerCase(); } +/** + * Convert a file extension to a Sharp image format. + * + * @param {String} ext + * + * @returns {String} + * @throws {Error} if the file extension is not supported + */ function extToFormat(ext: string): keyof FormatEnum { const format = imageFormatMap[ext]; if (!format) throw new Error('Unsupported file extension'); @@ -106,8 +131,10 @@ function extToFormat(ext: string): keyof FormatEnum { * https://futurestud.io/tutorials/node-js-check-if-a-path-is-a-file-url * * @param {String|URL} path + * The path to check. * * @returns {Boolean} + * True if the path is a web URI, false otherwise. */ function isWebURI(path: string | URL): boolean { try { @@ -118,30 +145,67 @@ function isWebURI(path: string | URL): boolean { } } +/** + * Base class for hit counter renderers. + * + * @class BaseHitCounterRenderer + * @implements {HitCounterRenderer} + */ class BaseHitCounterRenderer implements HitCounterRenderer { protected counter: HitCounter; protected config: HitCounterConfig; + /** + * Creates an instance of BaseHitCounterRenderer. + * + * @param {HitCounter} counter + * The hit counter instance. + * @param {HitCounterConfig} config + * The hit counter configuration. + */ constructor(counter: HitCounter, config: HitCounterConfig) { this.counter = counter; this.config = config; } + /** + * Render the hit counter. + */ async render(): Promise { throw new Error('Render not implemented'); } } +/** + * Image hit counter renderer. + * + * @class ImageHitCounterRenderer + * @extends {BaseHitCounterRenderer} + */ class ImageHitCounterRenderer extends BaseHitCounterRenderer { private imageFormat: keyof FormatEnum; private baseImage: Buffer; private baseImageMetadata: Metadata; + /** + * Creates an instance of ImageHitCounterRenderer. + * + * @param {HitCounter} counter + * The hit counter instance. + * @param {HitCounterConfig} config + * The hit counter configuration. + */ constructor(counter: HitCounter, config: HitCounterConfig) { super(counter, config); this.imageFormat = extToFormat(fileExt(this.config.imageFile)); } + /** + * Download the background image. + * + * @returns {Promise} + * The downloaded image. + */ private async downloadImage(): Promise { return new Promise((resolve, reject) => { let buf = Buffer.alloc(0); @@ -152,6 +216,15 @@ class ImageHitCounterRenderer extends BaseHitCounterRenderer { }); } + /** + * Load or cache the background image. + * + * @param {Boolean} [overwrite=false] + * Whether to overwrite the cached background image. + * + * @returns {Promise} + * The loaded or cached background image. + */ private async loadOrCacheBG(overwrite = false): Promise { if (fs.existsSync(this.config.backgroundCacheFile) && !overwrite) { return sharp(this.config.backgroundCacheFile); @@ -166,6 +239,12 @@ class ImageHitCounterRenderer extends BaseHitCounterRenderer { return image; } + /** + * Get the base image. + * + * @returns {Promise} + * The base image. + */ private async getBaseImage(): Promise { if (typeof this.baseImage !== 'undefined') { return this.baseImage; @@ -181,6 +260,17 @@ class ImageHitCounterRenderer extends BaseHitCounterRenderer { return this.baseImage; } + /** + * Create an SVG overlay. + * + * @param {Number} width + * The width of the overlay. + * @param {Number} height + * The height of the overlay. + * + * @returns {Buffer} + * The SVG overlay. + */ private createSvgOverlay(width: number, height: number): Buffer { const { textPositionX, textPositionY, fontFace, fontSize, customText, secondaryText, frameColorRGB, drawFrame } = this.config; const frame = drawFrame @@ -198,15 +288,34 @@ class ImageHitCounterRenderer extends BaseHitCounterRenderer { return Buffer.from(svg); } - private createSVGNumberOverlay(hits: number, imgWidth: number, imgHeight: number): Buffer { + /** + * Create an SVG number overlay. + * + * @param {Number} hits + * The number of hits. + * @param {Number} width + * The width of the image. + * @param {Number} height + * The height of the image. + * + * @returns {Buffer} + * The SVG number overlay. + */ + private createSVGNumberOverlay(hits: number, width: number, height: number): Buffer { const { numberPositionX, numberPositionY, fontFace, fontSize, numberColorRGB } = this.config; const svg = ` - + ${hits} `; return Buffer.from(svg); } + /** + * Render the hit counter. + * + * @returns {Promise} + * The rendered hit counter. + */ async render(): Promise { const hits = await this.counter.getHits(); return await sharp(await this.getBaseImage()) @@ -222,6 +331,11 @@ class ImageHitCounterRenderer extends BaseHitCounterRenderer { } } +/** + * Hit counter class. + * + * @class HitCounter + */ class HitCounter { private db: DB; private renderer: BaseHitCounterRenderer; @@ -229,6 +343,14 @@ class HitCounter { private mimeType: string; private config: HitCounterConfig; + /** + * Creates an instance of HitCounter. + * + * @param {Type} renderer + * The hit counter renderer. + * @param {HitCounterConfig} [config=defaultHitCounterConfig] + * The hit counter configuration. + */ constructor(renderer: Type, config: HitCounterConfig = defaultHitCounterConfig) { config = { ...defaultHitCounterConfig, ...config }; this.config = config; @@ -238,6 +360,15 @@ class HitCounter { this.preparedStatements = this.prepareStatements(); } + /** + * Initialize the database connection. + * + * @param {String} filePath + * The path to the database file. + * + * @returns {DB} + * The database instance. + */ private initDB(filePath: string): DB { const db = new Database(filePath, {fileMustExist: false}); db.pragma('locking_mode = EXCLUSIVE'); @@ -246,6 +377,12 @@ class HitCounter { return db; } + /** + * Prepare the SQL statements. + * + * @returns {Record} + * The prepared statements. + */ private prepareStatements(): Record { this.db.exec(` CREATE TABLE IF NOT EXISTS hit_counter ( @@ -266,27 +403,54 @@ class HitCounter { }; } + /** + * Get the number of hits. + * + * @returns {Promise} + * The number of hits + */ async getHits(): Promise { const result = this.preparedStatements.get.get(this.config.siteId) as HitResult; return result.hits; } + /** + * Increment the hit counter. + */ async increment(): Promise { this.preparedStatements.increment.run(this.config.siteId); } + /** + * Reset the hit counter + */ async reset(): Promise { this.preparedStatements.reset.run(this.config.siteId); } + /** + * Render the hit counter. + * + * @returns {Promise} + * The rendered hit counter. + */ async render(): Promise { return this.renderer.render(); } + /** + * Get the MIME type of the hit counter image. + * + * @returns {String} + * The MIME type. + */ getMimeType(): string { return this.mimeType; } + /** + * Destructor. + */ finalize(): void { this.db.close(); } From a2e698d2f6477e14e8f759129db0cc37db7c2049 Mon Sep 17 00:00:00 2001 From: zeyus Date: Thu, 7 Nov 2024 15:59:14 +0100 Subject: [PATCH 09/11] Made config first constructor arg, no longer necessary to pass any arguments. --- node/README.md | 11 ++++++----- node/src/hit-counter.ts | 8 ++++---- node/src/index.ts | 4 ++-- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/node/README.md b/node/README.md index d81af7a..e7650f5 100644 --- a/node/README.md +++ b/node/README.md @@ -90,7 +90,7 @@ interface HitCounterConfig { ## Usage ```ts -import { HitCounter, ImageHitCounterRenderer } from "./hit-counter"; +import { HitCounter } from "./hit-counter"; // default config @@ -113,13 +113,14 @@ defaultHitCounterConfig = { secondaryTextColorRGB: { r: 0, g: 255, b: 0 }, numberColorRGB: { r: 255, g: 255, b: 255 }, frameColorRGB: { r: 255, g: 0, b: 0 }, - drawFrame: true, - renderer: ImageHitCounterRenderer + drawFrame: true }; // the default config will be used, but you can pass a custom config object as -// the second argument to the HitCounter constructor -const hitCounter = new HitCounter(ImageHitCounterRenderer); +// the first argument to the HitCounter constructor +// e.g. const hitCounter = new HitCounter(customConfig); +// the second argument is reserved for the renderer, but currently only the ImageHitCounterRenderer is implemented +const hitCounter = new HitCounter(); // increment the hit counter hitCounter.increment(); // async function diff --git a/node/src/hit-counter.ts b/node/src/hit-counter.ts index c6a5d5c..7ee0c7b 100644 --- a/node/src/hit-counter.ts +++ b/node/src/hit-counter.ts @@ -346,12 +346,12 @@ class HitCounter { /** * Creates an instance of HitCounter. * - * @param {Type} renderer - * The hit counter renderer. * @param {HitCounterConfig} [config=defaultHitCounterConfig] - * The hit counter configuration. + * The hit counter configuration. Defaults to defaultHitCounterConfig. + * @param {Type} renderer + * The hit counter renderer. Defaults to ImageHitCounterRenderer. */ - constructor(renderer: Type, config: HitCounterConfig = defaultHitCounterConfig) { + constructor(config: HitCounterConfig = defaultHitCounterConfig, renderer: Type = ImageHitCounterRenderer) { config = { ...defaultHitCounterConfig, ...config }; this.config = config; this.renderer = new renderer(this, config); diff --git a/node/src/index.ts b/node/src/index.ts index c3647a6..0f5eeee 100644 --- a/node/src/index.ts +++ b/node/src/index.ts @@ -1,9 +1,9 @@ import express, { Express, Request, Response } from "express"; -import { HitCounter, ImageHitCounterRenderer } from "./hit-counter"; +import { HitCounter } from "./hit-counter"; const app: Express = express(); const PORT: number = 8000; -const hitCounter = new HitCounter(ImageHitCounterRenderer); +const hitCounter = new HitCounter(); const imageMimeType = hitCounter.getMimeType(); app.get("/", (req: Request, res: Response) => { From 5d1d67836cd05684b0ca2d6a0b2afab43ccdbbf8 Mon Sep 17 00:00:00 2001 From: zeyus Date: Thu, 7 Nov 2024 16:02:15 +0100 Subject: [PATCH 10/11] remove no longer relevaant attribution. --- node/src/hit-counter.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/node/src/hit-counter.ts b/node/src/hit-counter.ts index 7ee0c7b..fc1c70d 100644 --- a/node/src/hit-counter.ts +++ b/node/src/hit-counter.ts @@ -123,18 +123,13 @@ function extToFormat(ext: string): keyof FormatEnum { } /** - * Determine whether the given `path` is a http(s) URL. This method - * accepts a string value and a URL instance. + * Check if a path is a web URI. * - * inspiration: - * Marcus Pöhls - * https://futurestud.io/tutorials/node-js-check-if-a-path-is-a-file-url - * * @param {String|URL} path - * The path to check. - * + * The path to check. + * * @returns {Boolean} - * True if the path is a web URI, false otherwise. + * Whether the path is a web URI. */ function isWebURI(path: string | URL): boolean { try { From a08c8616d1e72eb6704101d7347ba681a8c80849 Mon Sep 17 00:00:00 2001 From: zeyus Date: Thu, 7 Nov 2024 16:07:13 +0100 Subject: [PATCH 11/11] Readability. --- node/src/hit-counter.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/node/src/hit-counter.ts b/node/src/hit-counter.ts index fc1c70d..469da03 100644 --- a/node/src/hit-counter.ts +++ b/node/src/hit-counter.ts @@ -267,16 +267,16 @@ class ImageHitCounterRenderer extends BaseHitCounterRenderer { * The SVG overlay. */ private createSvgOverlay(width: number, height: number): Buffer { - const { textPositionX, textPositionY, fontFace, fontSize, customText, secondaryText, frameColorRGB, drawFrame } = this.config; + const { textPositionX, textPositionY, fontFace, fontSize, customText, secondaryText, textColorRGB, secondaryTextColorRGB, frameColorRGB, drawFrame, borderWidth } = this.config; const frame = drawFrame - ? `` + ? `` : ''; const svg = ` - ${customText} - ${secondaryText} + ${customText} + ${secondaryText} ${frame} `;