diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..35e3ed1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,27 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist_bundle
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+.TODO
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..ff2677e
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,6 @@
+{
+ "useTabs": true,
+ "singleQuote": true,
+ "trailingComma": "none",
+ "printWidth": 100
+}
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..16f8f36
--- /dev/null
+++ b/index.html
@@ -0,0 +1,59 @@
+
+
+
+
+
+ ALTCHA Analytics Script
+
+
+
+
+
+
+
+
+
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..2152568
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,2124 @@
+{
+ "name": "@altcha/tracker",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "@altcha/tracker",
+ "version": "0.1.0",
+ "license": "MIT",
+ "devDependencies": {
+ "jsdom": "^25.0.1",
+ "rimraf": "^6.0.1",
+ "typescript": "^5.5.3",
+ "vite": "^5.4.1",
+ "vitest": "^2.1.1"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-linux-x64-gnu": "4.18.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "dev": true,
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+ "dev": true
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz",
+ "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz",
+ "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz",
+ "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz",
+ "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz",
+ "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz",
+ "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz",
+ "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz",
+ "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz",
+ "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz",
+ "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz",
+ "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.18.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz",
+ "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz",
+ "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz",
+ "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz",
+ "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz",
+ "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
+ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
+ "dev": true
+ },
+ "node_modules/@vitest/expect": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.1.tgz",
+ "integrity": "sha512-YeueunS0HiHiQxk+KEOnq/QMzlUuOzbU1Go+PgAsHvvv3tUkJPm9xWt+6ITNTlzsMXUjmgm5T+U7KBPK2qQV6w==",
+ "dev": true,
+ "dependencies": {
+ "@vitest/spy": "2.1.1",
+ "@vitest/utils": "2.1.1",
+ "chai": "^5.1.1",
+ "tinyrainbow": "^1.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.1.tgz",
+ "integrity": "sha512-LNN5VwOEdJqCmJ/2XJBywB11DLlkbY0ooDJW3uRX5cZyYCrc4PI/ePX0iQhE3BiEGiQmK4GE7Q/PqCkkaiPnrA==",
+ "dev": true,
+ "dependencies": {
+ "@vitest/spy": "^2.1.0-beta.1",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.11"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@vitest/spy": "2.1.1",
+ "msw": "^2.3.5",
+ "vite": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.1.tgz",
+ "integrity": "sha512-SjxPFOtuINDUW8/UkElJYQSFtnWX7tMksSGW0vfjxMneFqxVr8YJ979QpMbDW7g+BIiq88RAGDjf7en6rvLPPQ==",
+ "dev": true,
+ "dependencies": {
+ "tinyrainbow": "^1.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.1.tgz",
+ "integrity": "sha512-uTPuY6PWOYitIkLPidaY5L3t0JJITdGTSwBtwMjKzo5O6RCOEncz9PUN+0pDidX8kTHYjO0EwUIvhlGpnGpxmA==",
+ "dev": true,
+ "dependencies": {
+ "@vitest/utils": "2.1.1",
+ "pathe": "^1.1.2"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.1.tgz",
+ "integrity": "sha512-BnSku1WFy7r4mm96ha2FzN99AZJgpZOWrAhtQfoxjUU5YMRpq1zmHRq7a5K9/NjqonebO7iVDla+VvZS8BOWMw==",
+ "dev": true,
+ "dependencies": {
+ "@vitest/pretty-format": "2.1.1",
+ "magic-string": "^0.30.11",
+ "pathe": "^1.1.2"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.1.tgz",
+ "integrity": "sha512-ZM39BnZ9t/xZ/nF4UwRH5il0Sw93QnZXd9NAZGRpIgj0yvVwPpLd702s/Cx955rGaMlyBQkZJ2Ir7qyY48VZ+g==",
+ "dev": true,
+ "dependencies": {
+ "tinyspy": "^3.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.1.tgz",
+ "integrity": "sha512-Y6Q9TsI+qJ2CC0ZKj6VBb+T8UPz593N113nnUykqwANqhgf3QkZeHFlusgKLTqrnVHbj/XDKZcDHol+dxVT+rQ==",
+ "dev": true,
+ "dependencies": {
+ "@vitest/pretty-format": "2.1.1",
+ "loupe": "^3.1.1",
+ "tinyrainbow": "^1.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",
+ "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
+ "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "dev": true
+ },
+ "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
+ },
+ "node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/cac": {
+ "version": "6.7.14",
+ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
+ "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/chai": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz",
+ "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==",
+ "dev": true,
+ "dependencies": {
+ "assertion-error": "^2.0.1",
+ "check-error": "^2.1.1",
+ "deep-eql": "^5.0.1",
+ "loupe": "^3.1.0",
+ "pathval": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/check-error": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
+ "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 16"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dev": true,
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+ "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/cssstyle": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.1.0.tgz",
+ "integrity": "sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==",
+ "dev": true,
+ "dependencies": {
+ "rrweb-cssom": "^0.7.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/data-urls": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
+ "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
+ "dev": true,
+ "dependencies": {
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "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,
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decimal.js": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz",
+ "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==",
+ "dev": true
+ },
+ "node_modules/deep-eql": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
+ "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "dev": true
+ },
+ "node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "dev": true
+ },
+ "node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/foreground-child": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz",
+ "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==",
+ "dev": true,
+ "dependencies": {
+ "cross-spawn": "^7.0.0",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "dev": true,
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "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,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/get-func-name": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
+ "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/glob": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz",
+ "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==",
+ "dev": true,
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^4.0.1",
+ "minimatch": "^10.0.0",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^2.0.0"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/html-encoding-sniffer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
+ "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
+ "dev": true,
+ "dependencies": {
+ "whatwg-encoding": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "dev": true,
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz",
+ "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==",
+ "dev": true,
+ "dependencies": {
+ "agent-base": "^7.0.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "dev": true
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true
+ },
+ "node_modules/jackspeak": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.2.tgz",
+ "integrity": "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==",
+ "dev": true,
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/jsdom": {
+ "version": "25.0.1",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz",
+ "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==",
+ "dev": true,
+ "dependencies": {
+ "cssstyle": "^4.1.0",
+ "data-urls": "^5.0.0",
+ "decimal.js": "^10.4.3",
+ "form-data": "^4.0.0",
+ "html-encoding-sniffer": "^4.0.0",
+ "http-proxy-agent": "^7.0.2",
+ "https-proxy-agent": "^7.0.5",
+ "is-potential-custom-element-name": "^1.0.1",
+ "nwsapi": "^2.2.12",
+ "parse5": "^7.1.2",
+ "rrweb-cssom": "^0.7.1",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^5.0.0",
+ "w3c-xmlserializer": "^5.0.0",
+ "webidl-conversions": "^7.0.0",
+ "whatwg-encoding": "^3.1.1",
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.0.0",
+ "ws": "^8.18.0",
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "canvas": "^2.11.2"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/loupe": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz",
+ "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==",
+ "dev": true,
+ "dependencies": {
+ "get-func-name": "^2.0.1"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "11.0.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.1.tgz",
+ "integrity": "sha512-CgeuL5uom6j/ZVrg7G/+1IXqRY8JXX4Hghfy5YE0EhoYQWvndP1kufu58cmZLNIDKnRhZrXfdS9urVWx98AipQ==",
+ "dev": true,
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.11",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz",
+ "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0"
+ }
+ },
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz",
+ "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "dev": true,
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "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
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.7",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
+ "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/nwsapi": {
+ "version": "2.2.12",
+ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.12.tgz",
+ "integrity": "sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==",
+ "dev": true
+ },
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "dev": true
+ },
+ "node_modules/parse5": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
+ "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==",
+ "dev": true,
+ "dependencies": {
+ "entities": "^4.4.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-scurry": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz",
+ "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^11.0.0",
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/pathe": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
+ "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
+ "dev": true
+ },
+ "node_modules/pathval": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz",
+ "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 14.16"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
+ "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
+ "dev": true
+ },
+ "node_modules/postcss": {
+ "version": "8.4.47",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
+ "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "nanoid": "^3.3.7",
+ "picocolors": "^1.1.0",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/rimraf": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz",
+ "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==",
+ "dev": true,
+ "dependencies": {
+ "glob": "^11.0.0",
+ "package-json-from-dist": "^1.0.0"
+ },
+ "bin": {
+ "rimraf": "dist/esm/bin.mjs"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz",
+ "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==",
+ "dev": true,
+ "dependencies": {
+ "@types/estree": "1.0.6"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.24.0",
+ "@rollup/rollup-android-arm64": "4.24.0",
+ "@rollup/rollup-darwin-arm64": "4.24.0",
+ "@rollup/rollup-darwin-x64": "4.24.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.24.0",
+ "@rollup/rollup-linux-arm-musleabihf": "4.24.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.24.0",
+ "@rollup/rollup-linux-arm64-musl": "4.24.0",
+ "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.24.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.24.0",
+ "@rollup/rollup-linux-x64-gnu": "4.24.0",
+ "@rollup/rollup-linux-x64-musl": "4.24.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.24.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.24.0",
+ "@rollup/rollup-win32-x64-msvc": "4.24.0",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz",
+ "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/rrweb-cssom": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz",
+ "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==",
+ "dev": true
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true
+ },
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "dev": true,
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true
+ },
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "dev": true,
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true
+ },
+ "node_modules/std-env": {
+ "version": "3.7.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz",
+ "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==",
+ "dev": true
+ },
+ "node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "dev": true,
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "node_modules/string-width-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "dev": true
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true
+ },
+ "node_modules/tinyexec": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.0.tgz",
+ "integrity": "sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==",
+ "dev": true
+ },
+ "node_modules/tinypool": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.1.tgz",
+ "integrity": "sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==",
+ "dev": true,
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz",
+ "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tinyspy": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz",
+ "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tldts": {
+ "version": "6.1.47",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.47.tgz",
+ "integrity": "sha512-R/K2tZ5MiY+mVrnSkNJkwqYT2vUv1lcT6wJvd2emGaMJ7PHUGRY4e3tUsdFCXgqxi2QgbHjL3yJgXCo40v9Hxw==",
+ "dev": true,
+ "dependencies": {
+ "tldts-core": "^6.1.47"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/tldts-core": {
+ "version": "6.1.47",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.47.tgz",
+ "integrity": "sha512-6SWyFMnlst1fEt7GQVAAu16EGgFK0cLouH/2Mk6Ftlwhv3Ol40L0dlpGMcnnNiiOMyD2EV/aF3S+U2nKvvLvrA==",
+ "dev": true
+ },
+ "node_modules/tough-cookie": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz",
+ "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==",
+ "dev": true,
+ "dependencies": {
+ "tldts": "^6.1.32"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz",
+ "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==",
+ "dev": true,
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.5.4",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
+ "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
+ "dev": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/vite": {
+ "version": "5.4.8",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz",
+ "integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==",
+ "dev": true,
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite-node": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.1.tgz",
+ "integrity": "sha512-N/mGckI1suG/5wQI35XeR9rsMsPqKXzq1CdUndzVstBj/HvyxxGctwnK6WX43NGt5L3Z5tcRf83g4TITKJhPrA==",
+ "dev": true,
+ "dependencies": {
+ "cac": "^6.7.14",
+ "debug": "^4.3.6",
+ "pathe": "^1.1.2",
+ "vite": "^5.0.0"
+ },
+ "bin": {
+ "vite-node": "vite-node.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/vitest": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.1.tgz",
+ "integrity": "sha512-97We7/VC0e9X5zBVkvt7SGQMGrRtn3KtySFQG5fpaMlS+l62eeXRQO633AYhSTC3z7IMebnPPNjGXVGNRFlxBA==",
+ "dev": true,
+ "dependencies": {
+ "@vitest/expect": "2.1.1",
+ "@vitest/mocker": "2.1.1",
+ "@vitest/pretty-format": "^2.1.1",
+ "@vitest/runner": "2.1.1",
+ "@vitest/snapshot": "2.1.1",
+ "@vitest/spy": "2.1.1",
+ "@vitest/utils": "2.1.1",
+ "chai": "^5.1.1",
+ "debug": "^4.3.6",
+ "magic-string": "^0.30.11",
+ "pathe": "^1.1.2",
+ "std-env": "^3.7.0",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^0.3.0",
+ "tinypool": "^1.0.0",
+ "tinyrainbow": "^1.2.0",
+ "vite": "^5.0.0",
+ "vite-node": "2.1.1",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "@vitest/browser": "2.1.1",
+ "@vitest/ui": "2.1.1",
+ "happy-dom": "*",
+ "jsdom": "*"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/w3c-xmlserializer": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
+ "dev": true,
+ "dependencies": {
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-encoding": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
+ "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
+ "dev": true,
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
+ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
+ "dev": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz",
+ "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==",
+ "dev": true,
+ "dependencies": {
+ "tr46": "^5.0.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
+ "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
+ "dev": true,
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/xml-name-validator": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+ "dev": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "dev": true
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..cbe8810
--- /dev/null
+++ b/package.json
@@ -0,0 +1,52 @@
+{
+ "name": "@altcha/tracker",
+ "description": "JavaScript client for ALTCHA Analytics.",
+ "version": "0.1.0",
+ "license": "MIT",
+ "author": {
+ "name": "Daniel Regeci",
+ "url": "https://altcha.org"
+ },
+ "homepage": "https://altcha.org",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/altcha-org/tracker"
+ },
+ "type": "module",
+ "keywords": [
+ "altcha",
+ "analytics",
+ "statistics",
+ "tracker",
+ "svelte"
+ ],
+ "files": [
+ "dist",
+ "dist_bundle"
+ ],
+ "main": "./dist/tracker.umd.cjs",
+ "module": "./dist/tracker.js",
+ "exports": {
+ ".": {
+ "import": "./dist/tracker.js",
+ "require": "./dist/tracker.umd.cjs"
+ }
+ },
+ "scripts": {
+ "dev": "vite",
+ "build": "rimraf dist && tsc && vite build -c vite.config.js",
+ "build:bundle": "rimraf dist_bundle && tsc && vite build -c vite.bundle.config.js",
+ "preview": "vite preview",
+ "test": "vitest run"
+ },
+ "devDependencies": {
+ "jsdom": "^25.0.1",
+ "rimraf": "^6.0.1",
+ "typescript": "^5.5.3",
+ "vite": "^5.4.1",
+ "vitest": "^2.1.1"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-linux-x64-gnu": "4.18.0"
+ }
+}
diff --git a/src/extensions/base.ext.ts b/src/extensions/base.ext.ts
new file mode 100644
index 0000000..67b5641
--- /dev/null
+++ b/src/extensions/base.ext.ts
@@ -0,0 +1,21 @@
+import type { Tracker } from '../tracker';
+import type { IBaseExtensionOptions } from '../types';
+
+export class BaseExtension {
+ protected lastEvent: TEvent | null = null;
+
+ constructor(
+ readonly tracker: Tracker,
+ readonly options: IBaseExtensionOptions = { disable: false }
+ ) {}
+
+ destroy() {}
+
+ getExitReason(_unload: boolean = false): string | undefined {
+ return undefined;
+ }
+
+ isLastEventRecent(threshold: number = 10000, ev = this.lastEvent) {
+ return !!ev && performance.now() - ev.timeStamp < threshold;
+ }
+}
diff --git a/src/extensions/click.ext.ts b/src/extensions/click.ext.ts
new file mode 100644
index 0000000..63068c9
--- /dev/null
+++ b/src/extensions/click.ext.ts
@@ -0,0 +1,39 @@
+import { BaseExtension } from './base.ext';
+import type { IBaseExtensionOptions } from '../types';
+import type { Tracker } from '../tracker';
+
+export class ClickExtension extends BaseExtension {
+ readonly #_onClick = this.onClick.bind(this);
+
+ constructor(tracker: Tracker, _options: IBaseExtensionOptions) {
+ super(tracker);
+ addEventListener('click', this.#_onClick, {
+ capture: true,
+ });
+ }
+
+ destroy(): void {
+ removeEventListener('click', this.#_onClick);
+ }
+
+ getExitReason() {
+ if (this.lastEvent && this.isLastEventRecent()) {
+ const target = this.lastEvent.target as HTMLElement | null;
+ const href = target?.getAttribute('data-link') || target?.getAttribute('href');
+ if (href) {
+ const url = new URL(href, location.origin);
+ if (url.hostname && url.hostname !== location.hostname) {
+ return this.tracker.sanitizeUrl(url);
+ }
+ }
+ }
+ return undefined;
+ }
+
+ onClick(ev: MouseEvent) {
+ const target = ev.target as HTMLElement | null;
+ if (target && target.tagName === 'A') {
+ this.lastEvent = ev;
+ }
+ }
+}
diff --git a/src/extensions/cookie.ext.ts b/src/extensions/cookie.ext.ts
new file mode 100644
index 0000000..8a14ed6
--- /dev/null
+++ b/src/extensions/cookie.ext.ts
@@ -0,0 +1,49 @@
+import { BaseExtension } from './base.ext';
+import type { ICookieExtensionOptions } from '../types';
+import type { Tracker } from '../tracker';
+
+export class CookieExtension extends BaseExtension {
+ readonly #cookieName: string;
+
+ readonly #cookieExpireDays: number;
+
+ readonly #cookiePath: string;
+
+ constructor(tracker: Tracker, options: ICookieExtensionOptions) {
+ super(tracker);
+ this.#cookieName = options.cookieName || '_altcha_visited';
+ this.#cookieExpireDays = options.cookieExpireDays || 30;
+ this.#cookiePath = options.cookiePath || '/';
+ const cookie = this.getCookie(this.#cookieName);
+ if (cookie === '1') {
+ this.tracker.returningVisitor = true;
+ }
+ this.setCookie(
+ this.#cookieName,
+ '1',
+ new Date(Date.now() + 86_400_000 * this.#cookieExpireDays)
+ );
+ }
+
+ destroy() {}
+
+ getCookie(name: string) {
+ const cookies = document.cookie.split(/;\s?/);
+ for (const cookie of cookies) {
+ if (cookie.startsWith(name + '=')) {
+ return cookie.slice(name.length + 1);
+ }
+ }
+ return null;
+ }
+
+ setCookie(name: string, value: string, expires: Date) {
+ document.cookie =
+ name +
+ '=' +
+ value +
+ '; expires=' +
+ expires.toUTCString() +
+ `; SameSite=Strict; path=${this.#cookiePath || '/'}`;
+ }
+}
diff --git a/src/extensions/hash.ext.ts b/src/extensions/hash.ext.ts
new file mode 100644
index 0000000..590a225
--- /dev/null
+++ b/src/extensions/hash.ext.ts
@@ -0,0 +1,20 @@
+import { BaseExtension } from './base.ext';
+import type { IBaseExtensionOptions } from '../types';
+import type { Tracker } from '../tracker';
+
+export class HashExtension extends BaseExtension {
+ readonly #_onHashChange = this.onHashChange.bind(this);
+
+ constructor(tracker: Tracker, _options: IBaseExtensionOptions) {
+ super(tracker);
+ addEventListener('hashchange', this.#_onHashChange);
+ }
+
+ destroy() {
+ removeEventListener('hashchange', this.#_onHashChange);
+ }
+
+ onHashChange() {
+ this.tracker.trackPageview();
+ }
+}
diff --git a/src/extensions/keyboard.ext.ts b/src/extensions/keyboard.ext.ts
new file mode 100644
index 0000000..47433df
--- /dev/null
+++ b/src/extensions/keyboard.ext.ts
@@ -0,0 +1,35 @@
+import { BaseExtension } from './base.ext';
+import type { IBaseExtensionOptions } from '../types';
+import type { Tracker } from '../tracker';
+
+export class KeyboardExtension extends BaseExtension {
+ readonly #_onKeyDown = this.onKeyDown.bind(this);
+
+ constructor(tracker: Tracker, _options: IBaseExtensionOptions) {
+ super(tracker);
+ addEventListener('keydown', this.#_onKeyDown);
+ }
+
+ destroy() {
+ removeEventListener('keydown', this.#_onKeyDown);
+ }
+
+ isLastKeyboardEventCtrl() {
+ return (
+ !!this.lastEvent &&
+ (this.lastEvent.ctrlKey || this.lastEvent.metaKey)
+ );
+ }
+
+ getExitReason(unload: boolean = false) {
+ if (unload && this.isLastEventRecent(100) && this.isLastKeyboardEventCtrl()) {
+ // return empty string to indicate "exit" reason
+ return '';
+ }
+ return undefined;
+ }
+
+ onKeyDown(ev: KeyboardEvent) {
+ this.lastEvent = ev;
+ }
+}
diff --git a/src/extensions/mouse.ext.ts b/src/extensions/mouse.ext.ts
new file mode 100644
index 0000000..ef4831b
--- /dev/null
+++ b/src/extensions/mouse.ext.ts
@@ -0,0 +1,59 @@
+import { BaseExtension } from './base.ext';
+import { PushstateExtension } from './pushstate.ext';
+import type { IBaseExtensionOptions } from '../types';
+import type { Tracker } from '../tracker';
+
+export class MouseExtension extends BaseExtension {
+ readonly #_onMouseEnter = this.onMouseEnter.bind(this);
+
+ readonly #_onMouseLeave = this.onMouseLeave.bind(this);
+
+ isMouseOut: boolean = false;
+
+ offsetX: number = -1;
+
+ offsetY: number = -1;
+
+ constructor(tracker: Tracker, _options: IBaseExtensionOptions) {
+ super(tracker);
+ document.body.addEventListener('mouseleave', this.#_onMouseLeave);
+ document.body.addEventListener('mouseenter', this.#_onMouseEnter);
+ }
+
+ destroy(): void {
+ document.body.removeEventListener('mouseleave', this.#_onMouseLeave);
+ document.body.removeEventListener('mouseenter', this.#_onMouseEnter);
+ }
+
+ getExitReason() {
+ if (this.isMouseOut) {
+ const pushStateExt = this.tracker.getExtension('pushstate') as PushstateExtension | undefined;
+ if (
+ pushStateExt &&
+ pushStateExt.lastPopStateEvent &&
+ pushStateExt.isLastEventRecent(100, pushStateExt.lastPopStateEvent)
+ ) {
+ // popstate event has been called, most likely a back button navigation
+ return undefined;
+ }
+ if (this.offsetX >= 0 && this.offsetX >= 0 && this.offsetX < 150) {
+ // top left corner of the screen, most likely navigation buttons such as a back button
+ return undefined;
+ }
+ return '';
+ }
+ return undefined;
+ }
+
+ onMouseEnter() {
+ this.isMouseOut = false;
+ this.offsetX = -1;
+ this.offsetY = -1;
+ }
+
+ onMouseLeave(ev: MouseEvent) {
+ this.isMouseOut = true;
+ this.offsetX = ev.clientX;
+ this.offsetY = ev.clientY;
+ }
+}
diff --git a/src/extensions/pushstate.ext.ts b/src/extensions/pushstate.ext.ts
new file mode 100644
index 0000000..d2c3949
--- /dev/null
+++ b/src/extensions/pushstate.ext.ts
@@ -0,0 +1,39 @@
+import { BaseExtension } from './base.ext';
+import type { IBaseExtensionOptions } from '../types';
+import type { Tracker } from '../tracker';
+
+export class PushstateExtension extends BaseExtension {
+ #originalPushState: typeof history.pushState | null = null;
+
+ readonly #_onPopState = this.onPopState.bind(this);
+
+ readonly #_onPushState = this.onPushState.bind(this);
+
+ lastPopStateEvent: PopStateEvent | null = null;
+
+ constructor(tracker: Tracker, _options: IBaseExtensionOptions) {
+ super(tracker);
+ const originalPushState = (this.#originalPushState = history.pushState);
+ history.pushState = (data, title, url) => {
+ this.#_onPushState();
+ originalPushState?.apply(history, [data, title, url]);
+ };
+ addEventListener('popstate', this.#_onPopState);
+ }
+
+ destroy() {
+ if (this.#originalPushState) {
+ history.pushState = this.#originalPushState;
+ }
+ removeEventListener('popstate', this.#_onPopState);
+ }
+
+ onPopState(ev: PopStateEvent) {
+ this.lastPopStateEvent = ev;
+ this.tracker.trackPageview();
+ }
+
+ onPushState() {
+ this.tracker.trackPageview();
+ }
+}
diff --git a/src/extensions/visibility.ext.ts b/src/extensions/visibility.ext.ts
new file mode 100644
index 0000000..87463b2
--- /dev/null
+++ b/src/extensions/visibility.ext.ts
@@ -0,0 +1,61 @@
+import { BaseExtension } from './base.ext';
+import type { IVivibilityExtensionOptions } from '../types';
+import type { Tracker } from '../tracker';
+
+export class VisibilityExtension extends BaseExtension {
+ readonly #_onVisibilityChange = this.onVisibilityChange.bind(this);
+
+ readonly #hiddenTimeout: number;
+
+ #timeout: ReturnType | null = null;
+
+ visibilityState: DocumentVisibilityState = document.visibilityState;
+
+ constructor(tracker: Tracker, options: IVivibilityExtensionOptions) {
+ super(tracker);
+ this.#hiddenTimeout = options.hiddenTimeout || 4000;
+ addEventListener('visibilitychange', this.#_onVisibilityChange);
+ }
+
+ destroy() {
+ removeEventListener('visibilitychange', this.#_onVisibilityChange);
+ }
+
+ getExitReason(unload: boolean = false) {
+ const minThreshold = 1000;
+ if (
+ unload &&
+ this.visibilityState === 'hidden' &&
+ this.lastEvent &&
+ performance.now() - this.lastEvent.timeStamp >= minThreshold
+ ) {
+ // if hidden for a while, unload event indicates closing the tab
+ // note: visibilitychange can also fire during navigation => ignore very recent events
+ return '';
+ }
+ return undefined;
+ }
+
+ onTimeout() {
+ if (document.visibilityState === 'hidden') {
+ // track pageview as exit when hidden on timeout
+ this.tracker.trackPageview({}, true);
+ }
+ }
+
+ onVisibilityChange(ev: Event) {
+ this.lastEvent = ev;
+ this.visibilityState = document.visibilityState;
+ if (this.tracker.isMobile) {
+ // set timeout only on mobile devices
+ if (this.#timeout) {
+ clearTimeout(this.#timeout);
+ }
+ if (document.visibilityState === 'hidden') {
+ this.#timeout = setTimeout(() => {
+ this.onTimeout();
+ }, this.#hiddenTimeout);
+ }
+ }
+ }
+}
diff --git a/src/main.ts b/src/main.ts
new file mode 100644
index 0000000..4d48d5f
--- /dev/null
+++ b/src/main.ts
@@ -0,0 +1,28 @@
+import { Tracker } from './tracker';
+import type { ITrackerOptions } from './types';
+
+const currentScript = document.currentScript;
+const region = currentScript?.getAttribute('src')?.match(/([^\.]+)\.altcha\.org/)?.[1];
+const attrs = currentScript?.getAttributeNames() || [];
+const options = attrs.reduce((acc, attr) => {
+ if (attr.startsWith('data-')) {
+ const name = attr.slice(5).replace(/\-([a-z])/g, (_, w: string) => {
+ return w.toUpperCase();
+ });
+ let value: string | boolean | null | undefined = currentScript?.getAttribute(attr);
+ if (value) {
+ if (value === 'true') {
+ value = true;
+ } else if (value === 'false') {
+ value = false;
+ }
+ acc[name] = value;
+ }
+ }
+ return acc;
+}, {} as Record);
+
+export const tracker = new Tracker({
+ apiUrl: region ? `https://${region}.altcha.org/api/v1/event` : undefined,
+ ...options
+} as ITrackerOptions);
diff --git a/src/tests/extensions/click.ext.test.ts b/src/tests/extensions/click.ext.test.ts
new file mode 100644
index 0000000..4ae158f
--- /dev/null
+++ b/src/tests/extensions/click.ext.test.ts
@@ -0,0 +1,69 @@
+/**
+ * @vitest-environment jsdom
+ */
+
+import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest';
+import { ClickExtension } from '../../extensions/click.ext';
+import { Tracker } from '../../tracker';
+
+describe('ClickExtension', () => {
+ const tracker = new Tracker({
+ globalName: null,
+ projectId: '1'
+ });
+
+ it('should create a new instance and attache click listener', () => {
+ const addEventListenerSpy = vi.spyOn(globalThis, 'addEventListener');
+ const result = new ClickExtension(tracker, {});
+ expect(result).toBeInstanceOf(ClickExtension);
+ expect(addEventListenerSpy).toHaveBeenCalledWith('click', expect.any(Function), {
+ capture: true,
+ });
+ });
+
+ describe('methods', () => {
+ const ext = new ClickExtension(tracker, {});
+
+ afterAll(() => {
+ if (ext) {
+ try {
+ ext.destroy();
+ } catch {}
+ }
+ });
+
+ describe('.destroy()', () => {
+ it('should remove click', () => {
+ const removeEventListenerSpy = vi.spyOn(globalThis, 'removeEventListener');
+ ext.destroy();
+ expect(removeEventListenerSpy).toHaveBeenCalledWith('click', expect.any(Function));
+ });
+ });
+
+ describe('.getExitReason()', () => {
+ let a: HTMLAnchorElement;
+
+ beforeEach(() => {
+ a = document.createElement('a');
+ });
+
+ it('should return outbound url if clicked on a A element with external URL', () => {
+ a.setAttribute('href', 'https://google.com/');
+ ext.onClick({
+ target: a,
+ timeStamp: performance.now(),
+ } as any);
+ expect(ext.getExitReason()).toEqual('https://google.com/');
+ });
+
+ it('should return undefined if clicked on a A element with internal URL', () => {
+ a.setAttribute('href', '/test');
+ ext.onClick({
+ target: a,
+ timeStamp: performance.now(),
+ } as any);
+ expect(ext.getExitReason()).toBeUndefined();
+ });
+ });
+ });
+});
diff --git a/src/tests/extensions/cookie.ext.test.ts b/src/tests/extensions/cookie.ext.test.ts
new file mode 100644
index 0000000..973aac3
--- /dev/null
+++ b/src/tests/extensions/cookie.ext.test.ts
@@ -0,0 +1,23 @@
+/**
+ * @vitest-environment jsdom
+ */
+
+import { describe, expect, it, vi } from 'vitest'
+import { CookieExtension } from '../../extensions/cookie.ext';
+import { Tracker } from '../../tracker';
+
+describe('CookieExtenstion', () => {
+ const tracker = new Tracker({
+ globalName: null,
+ projectId: '1',
+ });
+
+ it('should create a new instance and set new cookie', () => {
+ const getCookie = vi.spyOn(CookieExtension.prototype, 'getCookie');
+ const setCookie = vi.spyOn(CookieExtension.prototype, 'setCookie');
+ const result = new CookieExtension(tracker, {});
+ expect(result).toBeInstanceOf(CookieExtension);
+ expect(getCookie).toHaveBeenCalledOnce();
+ expect(setCookie).toHaveBeenCalledOnce();
+ })
+});
diff --git a/src/tests/extensions/hash.ext.test.ts b/src/tests/extensions/hash.ext.test.ts
new file mode 100644
index 0000000..21e226e
--- /dev/null
+++ b/src/tests/extensions/hash.ext.test.ts
@@ -0,0 +1,49 @@
+/**
+ * @vitest-environment jsdom
+ */
+
+import { afterAll, describe, expect, it, vi } from 'vitest';
+import { HashExtension } from '../../extensions/hash.ext';
+import { Tracker } from '../../tracker';
+
+describe('HashExtension', () => {
+ const tracker = new Tracker({
+ globalName: null,
+ projectId: '1'
+ });
+
+ it('should create a new instance and attache hashchange listener', () => {
+ const addEventListenerSpy = vi.spyOn(globalThis, 'addEventListener');
+ const result = new HashExtension(tracker, {});
+ expect(result).toBeInstanceOf(HashExtension);
+ expect(addEventListenerSpy).toHaveBeenCalledWith('hashchange', expect.any(Function));
+ });
+
+ describe('methods', () => {
+ const ext = new HashExtension(tracker, {});
+
+ afterAll(() => {
+ if (ext) {
+ try {
+ ext.destroy();
+ } catch {}
+ }
+ });
+
+ describe('.destroy()', () => {
+ it('should remove hashchange', () => {
+ const removeEventListenerSpy = vi.spyOn(globalThis, 'removeEventListener');
+ ext.destroy();
+ expect(removeEventListenerSpy).toHaveBeenCalledWith('hashchange', expect.any(Function));
+ });
+ });
+
+ describe('.onHashChange()', () => {
+ it('should call tracker.trackPageview()', () => {
+ const trackedPageviewSpy = vi.spyOn(tracker, 'trackPageview');
+ ext.onHashChange();
+ expect(trackedPageviewSpy).toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/src/tests/extensions/keyboard.ext.test.ts b/src/tests/extensions/keyboard.ext.test.ts
new file mode 100644
index 0000000..668d3f3
--- /dev/null
+++ b/src/tests/extensions/keyboard.ext.test.ts
@@ -0,0 +1,103 @@
+/**
+ * @vitest-environment jsdom
+ */
+
+import { afterAll, describe, expect, it, vi } from "vitest";
+import { KeyboardExtension } from "../../extensions/keyboard.ext";
+import { Tracker } from "../../tracker";
+
+describe("KeyboardExtenstion", () => {
+ const tracker = new Tracker({
+ globalName: null,
+ projectId: "1",
+ });
+
+ it("should create a new instance and attach keydown listener", () => {
+ const addEventListenerSpy = vi.spyOn(globalThis, "addEventListener");
+ const result = new KeyboardExtension(tracker, {});
+ expect(result).toBeInstanceOf(KeyboardExtension);
+ expect(addEventListenerSpy).toHaveBeenCalledWith(
+ "keydown",
+ expect.any(Function)
+ );
+ });
+
+ describe("methods", () => {
+ const ext = new KeyboardExtension(tracker, {});
+
+ afterAll(() => {
+ if (ext) {
+ try {
+ ext.destroy();
+ } catch {}
+ }
+ });
+
+ describe(".destroy()", () => {
+ it("should remove keydown", () => {
+ const removeEventListenerSpy = vi.spyOn(
+ globalThis,
+ "removeEventListener"
+ );
+ ext.destroy();
+ expect(removeEventListenerSpy).toHaveBeenCalledWith(
+ "keydown",
+ expect.any(Function)
+ );
+ });
+ });
+
+ describe(".isLastKeyboardEventCtrl()", () => {
+ it("should return true if CTRL", () => {
+ ext.onKeyDown({
+ ctrlKey: true,
+ timeStamp: performance.now(),
+ } as KeyboardEvent);
+ expect(ext.isLastKeyboardEventCtrl()).toBe(true);
+ });
+
+ it("should return true if META", () => {
+ ext.onKeyDown({
+ metaKey: true,
+ timeStamp: performance.now(),
+ } as KeyboardEvent);
+ expect(ext.isLastKeyboardEventCtrl()).toBe(true);
+ });
+ });
+
+ describe(".getExitReason()", () => {
+ it("should return undefined if CTRL key was pressed but without unload", () => {
+ ext.onKeyDown({
+ ctrlKey: true,
+ timeStamp: performance.now(),
+ } as KeyboardEvent);
+ expect(ext.getExitReason(false)).toBe(undefined);
+ });
+
+ it("should return undefined if CTRL key was pressed but a second ago", async () => {
+ ext.onKeyDown({
+ ctrlKey: true,
+ timeStamp: performance.now(),
+ } as KeyboardEvent);
+ await new Promise((r) => setTimeout(r, 1000));
+ expect(ext.getExitReason(true)).toBe(undefined);
+ });
+
+ it("should return empty string if CTRL key was pressed before unload", () => {
+ ext.onKeyDown({
+ ctrlKey: true,
+ timeStamp: performance.now(),
+ } as KeyboardEvent);
+ expect(ext.getExitReason(true)).toBe("");
+ });
+
+ it("should return empty string if META key was pressed before unload", () => {
+ ext.onKeyDown({
+ metaKey: true,
+ timeStamp: performance.now(),
+ } as KeyboardEvent);
+ expect(ext.getExitReason(true)).toBe("");
+ });
+ });
+ });
+});
diff --git a/src/tests/extensions/mouse.ext.test.ts b/src/tests/extensions/mouse.ext.test.ts
new file mode 100644
index 0000000..e5b45d5
--- /dev/null
+++ b/src/tests/extensions/mouse.ext.test.ts
@@ -0,0 +1,78 @@
+/**
+ * @vitest-environment jsdom
+ */
+
+import { afterAll, describe, expect, it, vi } from 'vitest'
+import { MouseExtension } from '../../extensions/mouse.ext';
+import { Tracker } from '../../tracker';
+
+describe('MouseExtenstion', () => {
+ const tracker = new Tracker({
+ globalName: null,
+ projectId: '1',
+ });
+
+ it('should create a new instance and attach mouseleave listener', () => {
+ const addEventListenerSpy = vi.spyOn(document.body, "addEventListener");
+ const result = new MouseExtension(tracker, {});
+ expect(result).toBeInstanceOf(MouseExtension);
+ expect(addEventListenerSpy).toHaveBeenCalledWith(
+ "mouseleave",
+ expect.any(Function)
+ );
+ });
+
+ describe("methods", () => {
+ const ext = new MouseExtension(tracker, {});
+
+ afterAll(() => {
+ if (ext) {
+ try {
+ ext.destroy();
+ } catch {}
+ }
+ });
+
+ describe(".destroy()", () => {
+ it("should remove mouseleave", () => {
+ const removeEventListenerSpy = vi.spyOn(
+ document.body,
+ "removeEventListener"
+ );
+ ext.destroy();
+ expect(removeEventListenerSpy).toHaveBeenCalledWith(
+ "mouseleave",
+ expect.any(Function)
+ );
+ });
+ });
+
+ describe('.getExitReason()', () => {
+ it('should return empty string if cursor is outside the viewport', () => {
+ ext.onMouseLeave({
+ clientX: 500,
+ clientY: -1,
+ } as MouseEvent);
+ expect(ext.getExitReason()).toBe('');
+ });
+
+ it('should return undefined if cursor is outside the viewport but in the top left corner (navigation buttons)', () => {
+ ext.onMouseLeave({
+ clientX: 100,
+ clientY: -1,
+ } as MouseEvent);
+ ext.onMouseEnter();
+ expect(ext.getExitReason()).toBe(undefined);
+ });
+
+ it('should return undefined if cursor is inside the viewport', () => {
+ ext.onMouseLeave({
+ clientX: 500,
+ clientY: -1,
+ } as MouseEvent);
+ ext.onMouseEnter();
+ expect(ext.getExitReason()).toBe(undefined);
+ });
+ });
+ });
+});
diff --git a/src/tests/extensions/pushstate.ext.test.ts b/src/tests/extensions/pushstate.ext.test.ts
new file mode 100644
index 0000000..f4beb17
--- /dev/null
+++ b/src/tests/extensions/pushstate.ext.test.ts
@@ -0,0 +1,56 @@
+/**
+ * @vitest-environment jsdom
+ */
+
+import { afterAll, describe, expect, it, vi } from 'vitest';
+import { PushstateExtension } from '../../extensions/pushstate.ext';
+import { Tracker } from '../../tracker';
+
+describe('PushstateExtension', () => {
+ const tracker = new Tracker({
+ globalName: null,
+ projectId: '1'
+ });
+
+ it('should create a new instance and attache popstate listener', () => {
+ const addEventListenerSpy = vi.spyOn(globalThis, 'addEventListener');
+ const result = new PushstateExtension(tracker, {});
+ expect(result).toBeInstanceOf(PushstateExtension);
+ expect(addEventListenerSpy).toHaveBeenCalledWith('popstate', expect.any(Function));
+ });
+
+ it('should replace history.pushState function', () => {
+ const originalPushState = history.pushState;
+ const ext = new PushstateExtension(tracker, {});
+ expect(history.pushState).not.toEqual(originalPushState);
+ ext.destroy();
+ });
+
+ describe('methods', () => {
+ const ext = new PushstateExtension(tracker, {});
+
+ afterAll(() => {
+ if (ext) {
+ try {
+ ext.destroy();
+ } catch {}
+ }
+ });
+
+ describe('.destroy()', () => {
+ it('should remove popstate', () => {
+ const removeEventListenerSpy = vi.spyOn(globalThis, 'removeEventListener');
+ ext.destroy();
+ expect(removeEventListenerSpy).toHaveBeenCalledWith('popstate', expect.any(Function));
+ });
+ });
+
+ describe('.onPushState()', () => {
+ it('should call tracker.trackPageview()', () => {
+ const trackedPageviewSpy = vi.spyOn(tracker, 'trackPageview');
+ ext.onPushState();
+ expect(trackedPageviewSpy).toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/src/tests/extensions/visibility.ext.test.ts b/src/tests/extensions/visibility.ext.test.ts
new file mode 100644
index 0000000..93c2997
--- /dev/null
+++ b/src/tests/extensions/visibility.ext.test.ts
@@ -0,0 +1,41 @@
+/**
+ * @vitest-environment jsdom
+ */
+
+import { afterAll, describe, expect, it, vi } from 'vitest';
+import { VisibilityExtension } from '../../extensions/visibility.ext';
+import { Tracker } from '../../tracker';
+
+describe('VisibilityExtension', () => {
+ const tracker = new Tracker({
+ globalName: null,
+ projectId: '1'
+ });
+
+ it('should create a new instance and attache visibilitychange listener', () => {
+ const addEventListenerSpy = vi.spyOn(globalThis, 'addEventListener');
+ const result = new VisibilityExtension(tracker, {});
+ expect(result).toBeInstanceOf(VisibilityExtension);
+ expect(addEventListenerSpy).toHaveBeenCalledWith('visibilitychange', expect.any(Function));
+ });
+
+ describe('methods', () => {
+ const ext = new VisibilityExtension(tracker, {});
+
+ afterAll(() => {
+ if (ext) {
+ try {
+ ext.destroy();
+ } catch {}
+ }
+ });
+
+ describe('.destroy()', () => {
+ it('should remove visibilitychange', () => {
+ const removeEventListenerSpy = vi.spyOn(globalThis, 'removeEventListener');
+ ext.destroy();
+ expect(removeEventListenerSpy).toHaveBeenCalledWith('visibilitychange', expect.any(Function));
+ });
+ });
+ });
+});
diff --git a/src/tests/tracker.test.ts b/src/tests/tracker.test.ts
new file mode 100644
index 0000000..9761be4
--- /dev/null
+++ b/src/tests/tracker.test.ts
@@ -0,0 +1,386 @@
+/**
+ * @vitest-environment jsdom
+ */
+
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import { Tracker } from '../tracker';
+import type { IEvent, ITrackerOptions } from '../types';
+
+describe('Tracker', () => {
+
+ describe('constructor', () => {
+ it('should throw error if projectId is not provided', () => {
+ expect(() => new Tracker({
+ globalName: null,
+ } as ITrackerOptions)).toThrow();
+ });
+
+ it('should throw error if projectId is an empty string', () => {
+ expect(() => new Tracker({
+ globalName: null,
+ projectId: '',
+ })).toThrow();
+ });
+
+ it('should create a new instance and attach pagehide and beforeunload listeners', () => {
+ const addEventListenerSpy = vi.spyOn(globalThis, "addEventListener");
+ expect(new Tracker({
+ globalName: null,
+ projectId: '1'
+ })).toBeInstanceOf(Tracker);
+ expect(addEventListenerSpy).toHaveBeenCalledWith(
+ "pagehide",
+ expect.any(Function)
+ );
+ expect(addEventListenerSpy).toHaveBeenCalledWith(
+ "beforeunload",
+ expect.any(Function)
+ );
+ });
+
+ it('should register a global reference to the instance', () => {
+ const t = new Tracker({
+ projectId: '1',
+ });
+ // @ts-expect-error
+ expect(globalThis[Tracker.DEAFAUL_GLOBAL_NAME]).toEqual(t);
+ t.destroy();
+ });
+
+ it('should throw if another global reference is already registered', () => {
+ const t = new Tracker({
+ projectId: '1',
+ });
+ expect(() => new Tracker({
+ projectId: '1',
+ } as ITrackerOptions)).toThrow();
+ t.destroy();
+ });
+
+ it('should register a global reference to the instance with a configured name', () => {
+ const t = new Tracker({
+ globalName: 'test',
+ projectId: '1',
+ });
+ // @ts-expect-error
+ expect(globalThis.test).toEqual(t);
+ t.destroy();
+ });
+
+ it('should load default extensions', () => {
+ const loadExtensionsSpy = vi.spyOn(Tracker.prototype, "loadExtensions");
+ const tracker = new Tracker({
+ globalName: null,
+ projectId: '1'
+ });
+ expect(loadExtensionsSpy).toHaveBeenCalledOnce();
+ expect(Tracker.DEFAULT_EXTENSIONS.length).toBeGreaterThan(0);
+ expect(Object.keys(tracker.extensions)).toEqual(Tracker.DEFAULT_EXTENSIONS);
+ });
+
+ it('should disable all extensions', () => {
+ const tracker = new Tracker({
+ globalName: null,
+ click: false,
+ keyboard: false,
+ mouse: false,
+ pushstate: false,
+ visibility: false,
+ projectId: '1'
+ });
+ expect(Object.keys(tracker.extensions).length).toEqual(0);
+ });
+
+ it('should enable only pushstate extension', () => {
+ const tracker = new Tracker({
+ globalName: null,
+ click: false,
+ keyboard: false,
+ mouse: false,
+ pushstate: true,
+ visibility: false,
+ projectId: '1'
+ });
+ expect(Object.keys(tracker.extensions)).toEqual(['pushstate']);
+ });
+ });
+
+ describe('properties', () => {
+ let tracker: Tracker;
+
+ afterEach(() => {
+ if (tracker) {
+ tracker.destroy();
+ }
+ });
+
+ beforeEach(() => {
+ tracker = new Tracker({
+ globalName: null,
+ projectId: '1',
+ });
+ vi.restoreAllMocks();
+ });
+
+ describe('.apiUrl', () => {
+ it('should return default API URL', () => {
+ expect(tracker.apiUrl).toEqual(Tracker.DEFAULT_API_URL);
+ });
+
+ it('should return custom API URL when configured', () => {
+ const apiUrl = 'http://example.com/event';
+ const t = new Tracker({
+ apiUrl,
+ globalName: null,
+ projectId: '1',
+ });
+ expect(t.apiUrl).toEqual(apiUrl);
+ t.destroy();
+ });
+ });
+
+ describe('.isDNTEnabled', () => {
+ it('should return true if navigator.doNotTrack is 1', () => {
+ // @ts-ignore
+ navigator.doNotTrack = '1';
+ expect(tracker.isDNTEnabled).toEqual(true);
+ // @ts-ignore
+ navigator.doNotTrack = '0';
+ });
+
+ it('should return true if navigator.globalPrivacyControl is true', () => {
+ // @ts-ignore
+ navigator.globalPrivacyControl = true;
+ expect(tracker.isDNTEnabled).toEqual(true);
+ // @ts-ignore
+ navigator.globalPrivacyControl = false;
+ });
+ });
+ });
+
+ describe('methods', () => {
+ let tracker: Tracker;
+
+ afterEach(() => {
+ if (tracker) {
+ tracker.destroy();
+ }
+ });
+
+ beforeEach(() => {
+ tracker = new Tracker({
+ globalName: null,
+ projectId: '1',
+ });
+ vi.restoreAllMocks();
+ });
+
+ describe('.destroy()', () => {
+ it('should destroy the instance and remove pagehide listener', () => {
+ const removeEventListenerSpy = vi.spyOn(
+ globalThis,
+ "removeEventListener"
+ );
+ tracker.destroy();
+ expect(removeEventListenerSpy).toHaveBeenCalledWith(
+ "pagehide",
+ expect.any(Function)
+ );
+ });
+ });
+
+ describe('.getBeaconPayload()', () => {
+ it('should return an object with events, time and projectId', () => {
+ const events: IEvent[] = [{
+ timestamp: Date.now(),
+ }];
+ expect(tracker.getBeaconPayload(events)).toEqual({
+ events,
+ projectId: tracker.options.projectId,
+ time: expect.any(Number),
+ });
+ });
+ });
+
+ describe('.getHostname()', () => {
+ it('should return current location hostnane (localhost by default in JSDOM)', () => {
+ expect(tracker.getHostname()).toEqual('localhost');
+ });
+ });
+
+ describe('.getPageviewOptions()', () => {
+ it('should return options for pageview event', () => {
+ expect(tracker.getPageviewOptions()).toEqual({
+ appVersion: undefined,
+ duration: expect.any(Number),
+ exit: false,
+ outbound: undefined,
+ referrer: expect.any(String),
+ returning: undefined,
+ view: expect.any(String),
+ });
+ });
+
+ it('should return exit reason from .getExitReason()', () => {
+ const getExitReasonSpy = vi.spyOn(tracker, 'getExitReason');
+ expect(tracker.getPageviewOptions().exit).toEqual(false);
+ expect(getExitReasonSpy).toHaveBeenCalled();
+ });
+
+ it('should return duration', () => {
+ tracker.lastPageLoadAt = performance.now() - 1100;
+ expect(tracker.getPageviewOptions().duration).toBeGreaterThan(1000);
+ });
+
+ it('should return referrer from .getReferrer()', () => {
+ const getReferrerSpy = vi.spyOn(tracker, 'getReferrer');
+ expect(tracker.getPageviewOptions().referrer).toEqual(tracker.getReferrer());
+ expect(getReferrerSpy).toHaveBeenCalled();
+ });
+
+ it('should return view for .getView()', () => {
+ const getViewSpy = vi.spyOn(tracker, 'getView');
+ expect(tracker.getPageviewOptions().view).toEqual(tracker.getView());
+ expect(getViewSpy).toHaveBeenCalled();
+ });
+ });
+
+ describe('.getReferrer()', () => {
+ it('should return the document referrer (empty string by default)', () => {
+ expect(tracker.getReferrer()).toEqual('');
+ });
+ });
+
+ describe('.getView()', () => {
+ it('should return current location href', () => {
+ expect(tracker.getView()).toEqual(location.href);
+ });
+
+ it('should return call .sanitizeUrl()', () => {
+ const sanitizeUrlSpy = vi.spyOn(tracker, 'sanitizeUrl');
+ expect(tracker.getView()).toEqual(location.href);
+ expect(sanitizeUrlSpy).toHaveBeenCalled();
+ });
+ });
+
+ describe('.hasExtension()', () => {
+ it('should return true if extension is loaded', () => {
+ expect(tracker.hasExtension('pushstate')).toEqual(true);
+ });
+ it('should return true if extension is not loaded', () => {
+ expect(tracker.hasExtension('nonexistent' as any)).toEqual(false);
+ });
+ });
+
+ describe('.log()', () => {
+ it('should not log anything if debug is disabled', () => {
+ const logSpy = vi.spyOn(console, 'log');
+ tracker.log('test');
+ expect(logSpy).not.toHaveBeenCalled();
+ });
+
+ it('should log if debug is enabled', () => {
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => void 0);
+ tracker.options.debug = true;
+ tracker.log('test');
+ expect(logSpy).toHaveBeenCalledOnce();
+ });
+ });
+
+ describe('.sanitizeUrl()', () => {
+ it('should remove search params and hash', () => {
+ const url = 'https://localhost:1234/test'
+ expect(tracker.sanitizeUrl(url + '?test=123#removethis')).toEqual(url);
+ });
+
+ it('should remove only non-whitelisted search params', () => {
+ const url = 'https://localhost:1234/test'
+ const t = new Tracker({
+ allowSearchParams: ['test1', 'test2'],
+ globalName: null,
+ projectId: '1'
+ });
+ expect(t.sanitizeUrl(url + '?abc=test&test1=1&test2=2&test3=3')).toEqual(url + '?test1=1&test2=2');
+ t.destroy();
+ });
+ });
+
+ describe('.trackEvent()', () => {
+ it('should record an event', () => {
+ expect(tracker.trackedEvents).toEqual(0);
+ tracker.trackEvent({
+ customEvent: 'test',
+ props: {
+ customerPlan: 'free',
+ },
+ });
+ expect(tracker.trackedEvents).toEqual(1);
+ expect(tracker.events.length).toEqual(1);
+ expect(tracker.events[0].timestamp).toBeGreaterThan(0);
+ });
+ });
+
+ describe('.trackPageview()', () => {
+ it('should record a pageview event', () => {
+ expect(tracker.trackedEvents).toEqual(0);
+ tracker.trackPageview();
+ expect(tracker.trackedEvents).toEqual(1);
+ expect(tracker.events.length).toEqual(1);
+ expect(tracker.events[0].timestamp).toBeGreaterThan(0);
+ });
+ });
+
+ describe('.flushEvents()', () => {
+ it('should reset the internal events array', () => {
+ tracker.trackEvent({
+ customEvent: 'test',
+ });
+ expect(tracker.events.length).toEqual(1);
+ tracker.flushEvents();
+ expect(tracker.events.length).toEqual(0);
+ });
+
+ it('should call sendBeacon() with recoded events', () => {
+ const sendBeaconSpy = vi.spyOn(tracker, 'sendBeacon');
+ tracker.trackEvent({
+ customEvent: 'test',
+ });
+ const events = [...tracker.events];
+ tracker.flushEvents();
+ expect(sendBeaconSpy).toHaveBeenCalledWith(events);
+ });
+ });
+
+ describe('.sendBeacon()', () => {
+ it('should call navigator.sendBeacon() with JSON data', () => {
+ let sendBeaconArgs: any[] = [];
+ navigator.sendBeacon = (...args: any[]) => {
+ sendBeaconArgs = args;
+ return true;
+ };
+ const sendBeaconSpy = vi.spyOn(navigator, 'sendBeacon');
+ tracker.trackEvent({
+ customEvent: 'test',
+ });
+ const events = [...tracker.events];
+ tracker.flushEvents();
+ expect(sendBeaconSpy).toHaveBeenCalledWith(tracker.apiUrl, expect.any(String));
+ expect(JSON.parse(sendBeaconArgs[1])).toEqual({
+ events,
+ projectId: tracker.options.projectId,
+ time: expect.any(Number),
+ })
+ });
+ });
+
+ describe('.onPageHide()', () => {
+ it('should track pageview and flush events', () => {
+ const trackPageviewSpy = vi.spyOn(tracker, 'trackPageview');
+ const flushEventsSpy = vi.spyOn(tracker, 'flushEvents');
+ tracker.onPageHide();
+ expect(trackPageviewSpy).toHaveBeenCalledWith({}, true);
+ expect(flushEventsSpy).toHaveBeenCalled();
+ });
+ });
+ });
+});
\ No newline at end of file
diff --git a/src/tracker.ts b/src/tracker.ts
new file mode 100644
index 0000000..4c7d59b
--- /dev/null
+++ b/src/tracker.ts
@@ -0,0 +1,286 @@
+import { BaseExtension } from './extensions/base.ext';
+import { ClickExtension } from './extensions/click.ext';
+import { CookieExtension } from './extensions/cookie.ext';
+import { HashExtension } from './extensions/hash.ext';
+import { KeyboardExtension } from './extensions/keyboard.ext';
+import { MouseExtension } from './extensions/mouse.ext';
+import { PushstateExtension } from './extensions/pushstate.ext';
+import { VisibilityExtension } from './extensions/visibility.ext';
+import type { IBaseExtensionOptions, IEvent, ITrackerOptions } from './types';
+
+type TExtentionName = keyof typeof Tracker.EXTENSIONS;
+
+export class Tracker {
+ static readonly EXTENSIONS = {
+ click: ClickExtension,
+ cookie: CookieExtension,
+ hash: HashExtension,
+ keyboard: KeyboardExtension,
+ mouse: MouseExtension,
+ pushstate: PushstateExtension,
+ visibility: VisibilityExtension,
+ } as const;
+
+ static readonly DEFAULT_API_URL: string = 'https://eu.altcha.org/api/v1/event';
+
+ static readonly DEFAULT_EXTENSIONS: TExtentionName[] = [
+ 'click',
+ 'keyboard',
+ 'mouse',
+ 'pushstate',
+ 'visibility',
+ ];
+
+ static readonly DEAFAUL_GLOBAL_NAME: string = 'altchaTracker';
+
+ readonly #_onPageHide = this.onPageHide.bind(this);
+
+ readonly isMobile: boolean = /Mobi|Android|iPhone|iPad|iPod|Opera Mini/i.test(navigator.userAgent || '');
+
+ events: IEvent[] = [];
+
+ extensions = {} as Record>;
+
+ globalName: string | null = null;
+
+ lastPageLoadAt: number = performance.now();
+
+ lastPageview: string | null = null;
+
+ returningVisitor: boolean | null = null;
+
+ trackedEvents: number = 0;
+
+ trackedPageviews: number = 0;
+
+ get apiUrl() {
+ return this.options.apiUrl || Tracker.DEFAULT_API_URL;
+ }
+
+ get isDNTEnabled() {
+ return (
+ ('doNotTrack' in navigator && navigator.doNotTrack === '1') ||
+ ('globalPrivacyControl' in navigator && navigator.globalPrivacyControl === true)
+ );
+ }
+
+ constructor(readonly options: ITrackerOptions) {
+ if (!options.projectId) {
+ throw new Error('Parameter projectId required.');
+ }
+ this.#registerGlobalName();
+ if (this.isDNTEnabled && this.options.respectDNT === true) {
+ this.log('DoNotTrack enabled.');
+ } else {
+ this.loadExtensions();
+ // attach both pagehide and beforeunload listeners to handle inconsitent behaviour across browsers
+ // safari does not fire beforeunload after clicking on an external link
+ // brave does not fire pagehide after clicking on an external link
+ addEventListener('pagehide', this.#_onPageHide);
+ addEventListener('beforeunload', this.#_onPageHide);
+ }
+ }
+
+ destroy() {
+ this.flushEvents();
+ for (const ext in this.extensions) {
+ this.extensions[ext as TExtentionName].destroy();
+ }
+ this.extensions = {} as Record>;
+ removeEventListener('pagehide', this.#_onPageHide);
+ removeEventListener('beforeunload', this.#_onPageHide);
+ if (this.globalName) {
+ // @ts-expect-error
+ delete globalThis[this.globalName];
+ }
+ }
+
+ flushEvents() {
+ const events = this.events.splice(0);
+ if (events.length) {
+ this.sendBeacon(events);
+ }
+ }
+
+ getBeaconPayload(events: IEvent[]) {
+ return {
+ events,
+ projectId: this.options.projectId,
+ time: Date.now(),
+ uniqueId: this.options.uniqueId
+ };
+ }
+
+ getExitReason(unload: boolean = false) {
+ for (const ext in this.extensions) {
+ const result = this.extensions[ext as TExtentionName].getExitReason(unload);
+ if (result !== undefined) {
+ this.log('exit reason:', { ext, result });
+ return result;
+ }
+ }
+ return undefined;
+ }
+
+ getExtension(name: TExtentionName) {
+ return this.extensions[name];
+ }
+
+ getHostname() {
+ return location.hostname;
+ }
+
+ getOrigin() {
+ return location.origin;
+ }
+
+ getView() {
+ return this.sanitizeUrl(location.href);
+ }
+
+ getPageviewOptions(unload: boolean = false) {
+ const exitReason = this.getExitReason(unload);
+ const referrer = this.getReferrer();
+ const origin = this.getOrigin();
+ return {
+ appVersion: this.options.appVersion,
+ exit: exitReason !== undefined,
+ outbound: exitReason,
+ duration: Math.max(0, Math.floor(performance.now() - this.lastPageLoadAt)),
+ referrer: referrer && new URL(referrer, origin).origin === origin ? '' : referrer,
+ returning: this.returningVisitor === null ? undefined : this.returningVisitor,
+ view: this.getView()
+ };
+ }
+
+ getReferrer() {
+ return document.referrer;
+ }
+
+ hasExtension(name: TExtentionName) {
+ return !!this.getExtension(name);
+ }
+
+ loadExtensions() {
+ for (const ext in Tracker.EXTENSIONS) {
+ let extOptions: IBaseExtensionOptions | boolean =
+ this.options?.[ext as TExtentionName] !== undefined
+ ? this.options[ext as TExtentionName]!
+ : ({} as IBaseExtensionOptions);
+ if (typeof extOptions === 'boolean') {
+ extOptions = {
+ disable: !extOptions
+ };
+ } else {
+ extOptions.disable =
+ extOptions.disable === undefined
+ ? !Tracker.DEFAULT_EXTENSIONS.includes(ext as TExtentionName)
+ : extOptions.disable;
+ }
+ if (extOptions.disable !== true) {
+ this.extensions[ext as TExtentionName] = new Tracker.EXTENSIONS[ext as TExtentionName](
+ this,
+ extOptions
+ );
+ }
+ }
+ }
+
+ log(...args: any[]) {
+ if (this.options.debug) {
+ console.log('[ALTCHA Tracker]', ...args);
+ }
+ }
+
+ sanitizeUrl(url: string | URL) {
+ url = new URL(url);
+ if (this.options.allowSearchParams?.length && url.hostname === this.getHostname()) {
+ // remove only non-whitelisted params (only for current hostname)
+ for (const [param] of url.searchParams) {
+ if (!this.options.allowSearchParams.includes(param)) {
+ url.searchParams.delete(param);
+ }
+ }
+ } else {
+ // remove all search params
+ url.search = '';
+ }
+ if (!this.hasExtension('hash')) {
+ url.hash = '';
+ }
+ return url.toString();
+ }
+
+ sendBeacon(events: IEvent[]) {
+ if ('sendBeacon' in navigator) {
+ return navigator.sendBeacon(this.apiUrl, JSON.stringify(this.getBeaconPayload(events)));
+ }
+ }
+
+ trackEvent(options: Partial = {}, unload: boolean = false) {
+ const event = {
+ timestamp: Date.now(),
+ ...options
+ };
+ this.events.push(event);
+ this.trackedEvents += 1;
+ this.log('trackEvent', event);
+ if (unload) {
+ this.flushEvents();
+ }
+ return true;
+ }
+
+ trackPageview(options: Partial = {}, unload: boolean = false) {
+ const pageviewOptions = this.getPageviewOptions(unload);
+ if (
+ this.events.length &&
+ pageviewOptions.duration < 100 &&
+ this.events[this.events.length - 1].view === pageviewOptions.view
+ ) {
+ this.log('duplicate pageview', pageviewOptions);
+ return false;
+ }
+ this.log('trackPageview', pageviewOptions);
+ this.trackEvent(
+ {
+ ...pageviewOptions,
+ ...options
+ },
+ unload
+ );
+ this.trackedPageviews += 1;
+ this.lastPageLoadAt = performance.now();
+ this.lastPageview = pageviewOptions.view;
+ if (pageviewOptions.exit) {
+ // reset pageview counter to recognize the new enter event after back button
+ this.trackedPageviews = 0;
+ }
+ return true;
+ }
+
+ onPageHide() {
+ if (this.lastPageview !== this.getView()) {
+ // track only if the path is different from the previously recorder one.
+ // This is workaround for Safari's Back button from external link which fires pagehide on load
+ this.trackPageview({}, true);
+ }
+ }
+
+ #registerGlobalName() {
+ this.globalName =
+ this.options.globalName === undefined
+ ? Tracker.DEAFAUL_GLOBAL_NAME
+ : this.options.globalName || null;
+ if (this.globalName) {
+ // @ts-expect-error
+ if (globalThis[this.globalName]) {
+ throw new Error(
+ 'Another instance of the Tracker is already present in globalThis. Set globalName:null to disable global reference.'
+ );
+ }
+ // @ts-expect-error
+ globalThis[this.globalName] = this;
+ }
+ }
+}
diff --git a/src/types.ts b/src/types.ts
new file mode 100644
index 0000000..ea11996
--- /dev/null
+++ b/src/types.ts
@@ -0,0 +1,41 @@
+export interface IBaseExtensionOptions {
+ disable?: boolean;
+}
+
+export interface ICookieExtensionOptions extends IBaseExtensionOptions {
+ cookieExpireDays?: number;
+ cookieName?: string;
+ cookiePath?: string;
+}
+
+export interface IVivibilityExtensionOptions extends IBaseExtensionOptions {
+ hiddenTimeout?: number;
+}
+
+export interface IEvent {
+ customEvent?: string;
+ duration?: number;
+ exit?: boolean;
+ props?: Record;
+ returning?: boolean;
+ timestamp: number;
+ view?: string;
+}
+
+export interface ITrackerOptions {
+ allowSearchParams?: string[];
+ apiUrl?: string;
+ appVersion?: string;
+ click?: IBaseExtensionOptions | boolean;
+ cookie?: ICookieExtensionOptions | boolean;
+ debug?: boolean;
+ globalName?: string | null | false;
+ hash?: IBaseExtensionOptions | boolean;
+ keyboard?: IBaseExtensionOptions | boolean;
+ mouse?: IBaseExtensionOptions | boolean;
+ projectId: string;
+ pushstate?: IBaseExtensionOptions | boolean;
+ respectDNT?: boolean;
+ uniqueId?: string;
+ visibility?: IVivibilityExtensionOptions | boolean;
+}
\ No newline at end of file
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..0511b9f
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"]
+}
diff --git a/vite.bundle.config.js b/vite.bundle.config.js
new file mode 100644
index 0000000..d88b5a0
--- /dev/null
+++ b/vite.bundle.config.js
@@ -0,0 +1,13 @@
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ build: {
+ outDir: 'dist_bundle',
+ lib: {
+ entry: 'src/main.ts',
+ name: 'AltchaAnalyticsTracker',
+ fileName: () => 'tracker.js',
+ formats: ['umd']
+ },
+ },
+})
\ No newline at end of file
diff --git a/vite.config.js b/vite.config.js
new file mode 100644
index 0000000..eb991a4
--- /dev/null
+++ b/vite.config.js
@@ -0,0 +1,11 @@
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ build: {
+ lib: {
+ entry: 'src/tracker.ts',
+ name: 'AltchaAnalyticsTracker',
+ formats: ['es', 'umd']
+ },
+ },
+})
\ No newline at end of file