diff --git a/.gitignore b/.gitignore index a61cd08..e01c404 100644 --- a/.gitignore +++ b/.gitignore @@ -103,6 +103,9 @@ web_modules/ # Optional npm cache directory .npm +# Optional npm package-lock.json +package-lock.json + # Optional eslint cache .eslintcache diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2f8c7ac..c2670a2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,16 +11,22 @@ repos: hooks: - id: shellcheck -- repo: https://github.com/pre-commit/mirrors-eslint - rev: v10.0.3 +- repo: local + hooks: + - id: prettier + name: prettier + entry: npx prettier --write + language: system + files: "\\.(js|jsx|ts|tsx)$" + +- repo: local hooks: - id: eslint + name: eslint + entry: npx eslint --fix + language: system files: "\\.(js|jsx|ts|tsx)$" - exclude: node_modules/ -# Use stylelint (local) instead of the deprecated scss-lint Ruby gem which -# cannot parse modern Sass `@use` and module syntax. This invokes the -# project's installed `stylelint` via `npx` so the devDependency is used. - repo: local hooks: - id: stylelint diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 1b26413..0000000 --- a/package-lock.json +++ /dev/null @@ -1,3283 +0,0 @@ -{ - "name": "@hamradio/aprs", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@hamradio/aprs", - "version": "1.0.0", - "license": "MIT", - "devDependencies": { - "@eslint/js": "^10.0.1", - "@vitest/coverage-v8": "^4.0.18", - "eslint": "^10.0.3", - "globals": "^17.4.0", - "tsup": "^8.5.1", - "typescript": "^5.9.3", - "typescript-eslint": "^8.57.0", - "vitest": "^4.0.18" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", - "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", - "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", - "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", - "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", - "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", - "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", - "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", - "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", - "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", - "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", - "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", - "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", - "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", - "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", - "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", - "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", - "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", - "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", - "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", - "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", - "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", - "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", - "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", - "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", - "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", - "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", - "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.23.3", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz", - "integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^3.0.3", - "debug": "^4.3.1", - "minimatch": "^10.2.4" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz", - "integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.1.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/core": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz", - "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/js": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", - "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "eslint": "^10.0.0" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/@eslint/object-schema": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz", - "integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz", - "integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.1.1", - "levn": "^0.4.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", - "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", - "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", - "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", - "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", - "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", - "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", - "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", - "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", - "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", - "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", - "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", - "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", - "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", - "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", - "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", - "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", - "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", - "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", - "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", - "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", - "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", - "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", - "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", - "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", - "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } - }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/esrecurse": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", - "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.0.tgz", - "integrity": "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.57.0", - "@typescript-eslint/type-utils": "8.57.0", - "@typescript-eslint/utils": "8.57.0", - "@typescript-eslint/visitor-keys": "8.57.0", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.4.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.57.0", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.0.tgz", - "integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@typescript-eslint/scope-manager": "8.57.0", - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/typescript-estree": "8.57.0", - "@typescript-eslint/visitor-keys": "8.57.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.0.tgz", - "integrity": "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.57.0", - "@typescript-eslint/types": "^8.57.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz", - "integrity": "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/visitor-keys": "8.57.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz", - "integrity": "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.0.tgz", - "integrity": "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/typescript-estree": "8.57.0", - "@typescript-eslint/utils": "8.57.0", - "debug": "^4.4.3", - "ts-api-utils": "^2.4.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.0.tgz", - "integrity": "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz", - "integrity": "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.57.0", - "@typescript-eslint/tsconfig-utils": "8.57.0", - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/visitor-keys": "8.57.0", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.0.tgz", - "integrity": "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.57.0", - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/typescript-estree": "8.57.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz", - "integrity": "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.57.0", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@vitest/coverage-v8": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz", - "integrity": "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.0.18", - "ast-v8-to-istanbul": "^0.3.10", - "istanbul-lib-coverage": "^3.2.2", - "istanbul-lib-report": "^3.0.1", - "istanbul-reports": "^3.2.0", - "magicast": "^0.5.1", - "obug": "^2.1.1", - "std-env": "^3.10.0", - "tinyrainbow": "^3.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@vitest/browser": "4.0.18", - "vitest": "4.0.18" - }, - "peerDependenciesMeta": { - "@vitest/browser": { - "optional": true - } - } - }, - "node_modules/@vitest/expect": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", - "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.0.18", - "@vitest/utils": "4.0.18", - "chai": "^6.2.1", - "tinyrainbow": "^3.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", - "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "4.0.18", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0-0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/pretty-format": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", - "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^3.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", - "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "4.0.18", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", - "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.0.18", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", - "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", - "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.0.18", - "tinyrainbow": "^3.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, - "license": "MIT", - "peer": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true, - "license": "MIT" - }, - "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, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/ast-v8-to-istanbul": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", - "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.31", - "estree-walker": "^3.0.3", - "js-tokens": "^10.0.0" - } - }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/bundle-require": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", - "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "load-tsconfig": "^0.2.3" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "peerDependencies": { - "esbuild": ">=0.18" - } - }, - "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, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/consola": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", - "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.18.0 || >=16.10.0" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "dev": true, - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", - "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "peer": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.3", - "@esbuild/android-arm": "0.27.3", - "@esbuild/android-arm64": "0.27.3", - "@esbuild/android-x64": "0.27.3", - "@esbuild/darwin-arm64": "0.27.3", - "@esbuild/darwin-x64": "0.27.3", - "@esbuild/freebsd-arm64": "0.27.3", - "@esbuild/freebsd-x64": "0.27.3", - "@esbuild/linux-arm": "0.27.3", - "@esbuild/linux-arm64": "0.27.3", - "@esbuild/linux-ia32": "0.27.3", - "@esbuild/linux-loong64": "0.27.3", - "@esbuild/linux-mips64el": "0.27.3", - "@esbuild/linux-ppc64": "0.27.3", - "@esbuild/linux-riscv64": "0.27.3", - "@esbuild/linux-s390x": "0.27.3", - "@esbuild/linux-x64": "0.27.3", - "@esbuild/netbsd-arm64": "0.27.3", - "@esbuild/netbsd-x64": "0.27.3", - "@esbuild/openbsd-arm64": "0.27.3", - "@esbuild/openbsd-x64": "0.27.3", - "@esbuild/openharmony-arm64": "0.27.3", - "@esbuild/sunos-x64": "0.27.3", - "@esbuild/win32-arm64": "0.27.3", - "@esbuild/win32-ia32": "0.27.3", - "@esbuild/win32-x64": "0.27.3" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.3.tgz", - "integrity": "sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.3", - "@eslint/config-helpers": "^0.5.2", - "@eslint/core": "^1.1.1", - "@eslint/plugin-kit": "^0.6.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.14.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^9.1.2", - "eslint-visitor-keys": "^5.0.1", - "espree": "^11.1.1", - "esquery": "^1.7.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.4", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-scope": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", - "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@types/esrecurse": "^4.3.1", - "@types/estree": "^1.0.8", - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", - "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.16.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^5.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/fix-dts-default-cjs-exports": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", - "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "magic-string": "^0.30.17", - "mlly": "^1.7.4", - "rollup": "^4.34.8" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", - "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "17.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", - "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/joycon": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", - "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/js-tokens": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", - "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/load-tsconfig": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", - "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/magicast": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", - "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", - "source-map-js": "^1.2.1" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/mlly": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.1.tgz", - "integrity": "sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.16.0", - "pathe": "^2.0.3", - "pkg-types": "^1.3.1", - "ufo": "^1.6.3" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/obug": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", - "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", - "dev": true, - "funding": [ - "https://github.com/sponsors/sxzz", - "https://opencollective.com/debug" - ], - "license": "MIT" - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "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, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-types": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", - "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" - } - }, - "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", - "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" - } - ], - "license": "MIT", - "peer": true, - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-load-config": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", - "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.1.1" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "jiti": ">=1.21.0", - "postcss": ">=8.0.9", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - }, - "postcss": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "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, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/rollup": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", - "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.59.0", - "@rollup/rollup-android-arm64": "4.59.0", - "@rollup/rollup-darwin-arm64": "4.59.0", - "@rollup/rollup-darwin-x64": "4.59.0", - "@rollup/rollup-freebsd-arm64": "4.59.0", - "@rollup/rollup-freebsd-x64": "4.59.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", - "@rollup/rollup-linux-arm-musleabihf": "4.59.0", - "@rollup/rollup-linux-arm64-gnu": "4.59.0", - "@rollup/rollup-linux-arm64-musl": "4.59.0", - "@rollup/rollup-linux-loong64-gnu": "4.59.0", - "@rollup/rollup-linux-loong64-musl": "4.59.0", - "@rollup/rollup-linux-ppc64-gnu": "4.59.0", - "@rollup/rollup-linux-ppc64-musl": "4.59.0", - "@rollup/rollup-linux-riscv64-gnu": "4.59.0", - "@rollup/rollup-linux-riscv64-musl": "4.59.0", - "@rollup/rollup-linux-s390x-gnu": "4.59.0", - "@rollup/rollup-linux-x64-gnu": "4.59.0", - "@rollup/rollup-linux-x64-musl": "4.59.0", - "@rollup/rollup-openbsd-x64": "4.59.0", - "@rollup/rollup-openharmony-arm64": "4.59.0", - "@rollup/rollup-win32-arm64-msvc": "4.59.0", - "@rollup/rollup-win32-ia32-msvc": "4.59.0", - "@rollup/rollup-win32-x64-gnu": "4.59.0", - "@rollup/rollup-win32-x64-msvc": "4.59.0", - "fsevents": "~2.3.2" - } - }, - "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "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, - "license": "MIT", - "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, - "license": "MIT", - "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, - "license": "ISC" - }, - "node_modules/source-map": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">= 12" - } - }, - "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, - "license": "BSD-3-Clause", - "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, - "license": "MIT" - }, - "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "dev": true, - "license": "MIT" - }, - "node_modules/sucrase": { - "version": "3.35.1", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", - "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "tinyglobby": "^0.2.11", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "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, - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyrainbow": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", - "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true, - "license": "MIT", - "bin": { - "tree-kill": "cli.js" - } - }, - "node_modules/ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/tsup": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", - "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", - "dev": true, - "license": "MIT", - "dependencies": { - "bundle-require": "^5.1.0", - "cac": "^6.7.14", - "chokidar": "^4.0.3", - "consola": "^3.4.0", - "debug": "^4.4.0", - "esbuild": "^0.27.0", - "fix-dts-default-cjs-exports": "^1.0.0", - "joycon": "^3.1.1", - "picocolors": "^1.1.1", - "postcss-load-config": "^6.0.1", - "resolve-from": "^5.0.0", - "rollup": "^4.34.8", - "source-map": "^0.7.6", - "sucrase": "^3.35.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.11", - "tree-kill": "^1.2.2" - }, - "bin": { - "tsup": "dist/cli-default.js", - "tsup-node": "dist/cli-node.js" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@microsoft/api-extractor": "^7.36.0", - "@swc/core": "^1", - "postcss": "^8.4.12", - "typescript": ">=4.5.0" - }, - "peerDependenciesMeta": { - "@microsoft/api-extractor": { - "optional": true - }, - "@swc/core": { - "optional": true - }, - "postcss": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.0.tgz", - "integrity": "sha512-W8GcigEMEeB07xEZol8oJ26rigm3+bfPHxHvwbYUlu1fUDsGuQ7Hiskx5xGW/xM4USc9Ephe3jtv7ZYPQntHeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.57.0", - "@typescript-eslint/parser": "8.57.0", - "@typescript-eslint/typescript-estree": "8.57.0", - "@typescript-eslint/utils": "8.57.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/ufo": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", - "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vitest": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", - "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@vitest/expect": "4.0.18", - "@vitest/mocker": "4.0.18", - "@vitest/pretty-format": "4.0.18", - "@vitest/runner": "4.0.18", - "@vitest/snapshot": "4.0.18", - "@vitest/spy": "4.0.18", - "@vitest/utils": "4.0.18", - "es-module-lexer": "^1.7.0", - "expect-type": "^1.2.2", - "magic-string": "^0.30.21", - "obug": "^2.1.1", - "pathe": "^2.0.3", - "picomatch": "^4.0.3", - "std-env": "^3.10.0", - "tinybench": "^2.9.0", - "tinyexec": "^1.0.2", - "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.0.3", - "vite": "^6.0.0 || ^7.0.0", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@opentelemetry/api": "^1.9.0", - "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.0.18", - "@vitest/browser-preview": "4.0.18", - "@vitest/browser-webdriverio": "4.0.18", - "@vitest/ui": "4.0.18", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@opentelemetry/api": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser-playwright": { - "optional": true - }, - "@vitest/browser-preview": { - "optional": true - }, - "@vitest/browser-webdriverio": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/vitest/node_modules/tinyexec": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", - "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", - "dev": true, - "license": "MIT", - "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, - "license": "ISC", - "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, - "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/package.json b/package.json index 9936563..23fb7b2 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ ], "repository": { "type": "git", - "url": "https://git.maze.io/ham/aprs.js" + "url": "https://git.maze.io/ham/aprs.ts" }, "license": "MIT", "author": "Wijnand Modderman-Lenstra", @@ -38,7 +38,6 @@ "lint": "eslint .", "prepare": "npm run build" }, - "dependencies": {}, "devDependencies": { "@eslint/js": "^10.0.1", "@vitest/coverage-v8": "^4.0.18", @@ -48,5 +47,8 @@ "typescript": "^5.9.3", "typescript-eslint": "^8.57.0", "vitest": "^4.0.18" + }, + "dependencies": { + "@hamradio/packet": "^1.1.0" } } diff --git a/src/frame.ts b/src/frame.ts index d297fe5..bce0e6c 100644 --- a/src/frame.ts +++ b/src/frame.ts @@ -1,7 +1,19 @@ -import type { IAddress, IFrame, Payload, ITimestamp, PositionPayload, IPosition } from "./frame.types" -import { type PacketStructure, type PacketSegment, type PacketField, FieldType } from "./parser.types" +import type { Dissected, Segment, Field } from "@hamradio/packet"; +import { FieldType } from "@hamradio/packet"; +import type { + IAddress, + IFrame, + Payload, + ITimestamp, + PositionPayload, + MessagePayload, + IPosition, + ObjectPayload, + ItemPayload, + StatusPayload, +} from "./frame.types"; import { Position } from "./position"; -import { base91ToNumber } from "./parser" +import { base91ToNumber } from "./parser"; export class Timestamp implements ITimestamp { day?: number; @@ -9,19 +21,19 @@ export class Timestamp implements ITimestamp { hours: number; minutes: number; seconds?: number; - format: 'DHM' | 'HMS' | 'MDHM'; + format: "DHM" | "HMS" | "MDHM"; zulu?: boolean; constructor( hours: number, minutes: number, - format: 'DHM' | 'HMS' | 'MDHM', + format: "DHM" | "HMS" | "MDHM", options: { day?: number; month?: number; seconds?: number; zulu?: boolean; - } = {} + } = {}, ) { this.hours = hours; this.minutes = minutes; @@ -42,7 +54,7 @@ export class Timestamp implements ITimestamp { toDate(): Date { const now = new Date(); - if (this.format === 'DHM') { + if (this.format === "DHM") { // Day-Hour-Minute format (UTC) // Find the most recent occurrence of this day const currentYear = this.zulu ? now.getUTCFullYear() : now.getFullYear(); @@ -50,22 +62,58 @@ export class Timestamp implements ITimestamp { let date: Date; if (this.zulu) { - date = new Date(Date.UTC(currentYear, currentMonth, this.day!, this.hours, this.minutes, 0, 0)); + date = new Date( + Date.UTC( + currentYear, + currentMonth, + this.day!, + this.hours, + this.minutes, + 0, + 0, + ), + ); } else { - date = new Date(currentYear, currentMonth, this.day!, this.hours, this.minutes, 0, 0); + date = new Date( + currentYear, + currentMonth, + this.day!, + this.hours, + this.minutes, + 0, + 0, + ); } // If the date is in the future, it's from last month if (date > now) { if (this.zulu) { - date = new Date(Date.UTC(currentYear, currentMonth - 1, this.day!, this.hours, this.minutes, 0, 0)); + date = new Date( + Date.UTC( + currentYear, + currentMonth - 1, + this.day!, + this.hours, + this.minutes, + 0, + 0, + ), + ); } else { - date = new Date(currentYear, currentMonth - 1, this.day!, this.hours, this.minutes, 0, 0); + date = new Date( + currentYear, + currentMonth - 1, + this.day!, + this.hours, + this.minutes, + 0, + 0, + ); } } return date; - } else if (this.format === 'HMS') { + } else if (this.format === "HMS") { // Hour-Minute-Second format (UTC) // Use current date if (this.zulu) { @@ -91,11 +139,27 @@ export class Timestamp implements ITimestamp { } else { // MDHM format: Month-Day-Hour-Minute (local time) const currentYear = now.getFullYear(); - let date = new Date(currentYear, (this.month || 1) - 1, this.day!, this.hours, this.minutes, 0, 0); + let date = new Date( + currentYear, + (this.month || 1) - 1, + this.day!, + this.hours, + this.minutes, + 0, + 0, + ); // If date is in the future, it's from last year if (date > now) { - date = new Date(currentYear - 1, (this.month || 1) - 1, this.day!, this.hours, this.minutes, 0, 0); + date = new Date( + currentYear - 1, + (this.month || 1) - 1, + this.day!, + this.hours, + this.minutes, + 0, + 0, + ); } return date; @@ -108,32 +172,36 @@ export class Address implements IAddress { ssid: string = ""; isRepeated: boolean = false; - constructor(call: string, ssid: string | number = "", isRepeated: boolean = false) { + constructor( + call: string, + ssid: string | number = "", + isRepeated: boolean = false, + ) { this.call = call; - if (typeof ssid === 'number') { + if (typeof ssid === "number") { this.ssid = ssid.toString(); - } else if (typeof ssid === 'string') { + } else if (typeof ssid === "string") { this.ssid = ssid; } else { throw new Error("SSID must be a string or number"); } - if (typeof isRepeated !== 'boolean') { + if (typeof isRepeated !== "boolean") { throw new Error("isRepeated must be a boolean"); } this.isRepeated = isRepeated || false; } public toString(): string { - return `${this.call}${this.ssid ? '-' + this.ssid : ''}${this.isRepeated ? '*' : ''}`; + return `${this.call}${this.ssid ? "-" + this.ssid : ""}${this.isRepeated ? "*" : ""}`; } public static fromString(addr: string): Address { - const isRepeated = addr.endsWith('*'); + const isRepeated = addr.endsWith("*"); const baseAddr = isRepeated ? addr.slice(0, -1) : addr; - const parts = baseAddr.split('-'); + const parts = baseAddr.split("-"); const call = parts[0]; - const ssid = parts.length > 1 ? parts[1] : ''; + const ssid = parts.length > 1 ? parts[1] : ""; return new Address(call, ssid, isRepeated); } @@ -148,9 +216,15 @@ export class Frame implements IFrame { destination: Address; path: Address[]; payload: string; - private _routingSection?: PacketSegment; + private _routingSection?: Segment; - constructor(source: Address, destination: Address, path: Address[], payload: string, routingSection?: PacketSegment) { + constructor( + source: Address, + destination: Address, + path: Address[], + payload: string, + routingSection?: Segment, + ) { this.source = source; this.destination = destination; this.path = path; @@ -168,7 +242,7 @@ export class Frame implements IFrame { /** * Get or build routing section from cached data */ - private getRoutingSection(): PacketSegment | undefined { + private getRoutingSection(): Segment | undefined { return this._routingSection; } @@ -176,21 +250,22 @@ export class Frame implements IFrame { * Decode the APRS payload based on its data type identifier * Returns the decoded payload with optional structure for packet dissection */ - decode(withStructure?: boolean): Payload | null | { payload: Payload | null; structure: PacketStructure } { + decode( + withStructure?: boolean, + ): Payload | null | { payload: Payload | null; structure: Dissected } { if (!this.payload) { if (withStructure) { - const structure: PacketStructure = []; + const structure: Dissected = []; const routingSection = this.getRoutingSection(); if (routingSection) { structure.push(routingSection); // Add data type identifier section structure.push({ - name: 'Data Type Identifier', - data: new TextEncoder().encode(this.payload.charAt(0)), - fields: [ - { type: FieldType.CHAR, name: 'Identifier', size: 1 }, - ], + name: "Data Type Identifier", + data: new TextEncoder().encode(this.payload.charAt(0)).buffer, + isString: true, + fields: [{ type: FieldType.CHAR, name: "Identifier", length: 1 }], }); } return { payload: null, structure }; @@ -199,65 +274,79 @@ export class Frame implements IFrame { } const dataType = this.getDataTypeIdentifier(); + // eslint-disable-next-line no-useless-assignment let decodedPayload: Payload | null = null; - let payloadsegment: PacketSegment[] | undefined = undefined; + let payloadsegment: Segment[] | undefined = undefined; // TODO: Implement full decoding logic for each payload type switch (dataType) { - case '!': // Position without timestamp, no messaging - case '=': // Position without timestamp, with messaging - case '/': // Position with timestamp, no messaging - case '@': // Position with timestamp, with messaging - ({ payload: decodedPayload, segment: payloadsegment } = this.decodePosition(dataType, withStructure)); + case "!": // Position without timestamp, no messaging + case "=": // Position without timestamp, with messaging + case "/": // Position with timestamp, no messaging + case "@": // Position with timestamp, with messaging + ({ payload: decodedPayload, segment: payloadsegment } = + this.decodePosition(dataType, withStructure)); break; - case '`': // Mic-E current + case "`": // Mic-E current case "'": // Mic-E old - ({ payload: decodedPayload, segment: payloadsegment } = this.decodeMicE(withStructure)); + ({ payload: decodedPayload, segment: payloadsegment } = + this.decodeMicE(withStructure)); break; - case ':': // Message - ({ payload: decodedPayload, segment: payloadsegment } = this.decodeMessage(withStructure)); + case ":": // Message + ({ payload: decodedPayload, segment: payloadsegment } = + this.decodeMessage(withStructure)); break; - case ';': // Object - ({ payload: decodedPayload, segment: payloadsegment } = this.decodeObject(withStructure)); + case ";": // Object + ({ payload: decodedPayload, segment: payloadsegment } = + this.decodeObject(withStructure)); break; - case ')': // Item - ({ payload: decodedPayload, segment: payloadsegment } = this.decodeItem(withStructure)); + case ")": // Item + ({ payload: decodedPayload, segment: payloadsegment } = + this.decodeItem(withStructure)); break; - case '>': // Status - ({ payload: decodedPayload, segment: payloadsegment } = this.decodeStatus(withStructure)); + case ">": // Status + ({ payload: decodedPayload, segment: payloadsegment } = + this.decodeStatus(withStructure)); break; - case '?': // Query - ({ payload: decodedPayload, segment: payloadsegment } = this.decodeQuery(withStructure)); + case "?": // Query + ({ payload: decodedPayload, segment: payloadsegment } = + this.decodeQuery(withStructure)); break; - case 'T': // Telemetry - ({ payload: decodedPayload, segment: payloadsegment } = this.decodeTelemetry(withStructure)); + case "T": // Telemetry + ({ payload: decodedPayload, segment: payloadsegment } = + this.decodeTelemetry(withStructure)); break; - case '_': // Weather without position - ({ payload: decodedPayload, segment: payloadsegment } = this.decodeWeather(withStructure)); + case "_": // Weather without position + ({ payload: decodedPayload, segment: payloadsegment } = + this.decodeWeather(withStructure)); break; - case '$': // Raw GPS - ({ payload: decodedPayload, segment: payloadsegment } = this.decodeRawGPS(withStructure)); + case "$": // Raw GPS + ({ payload: decodedPayload, segment: payloadsegment } = + this.decodeRawGPS(withStructure)); break; - case '<': // Station capabilities - ({ payload: decodedPayload, segment: payloadsegment } = this.decodeCapabilities(withStructure)); + case "<": // Station capabilities + ({ payload: decodedPayload, segment: payloadsegment } = + this.decodeCapabilities(withStructure)); break; - case '{': // User-defined - ({ payload: decodedPayload, segment: payloadsegment } = this.decodeUserDefined(withStructure)); + case "{": // User-defined + ({ payload: decodedPayload, segment: payloadsegment } = + this.decodeUserDefined(withStructure)); break; - case '}': // Third-party - ({ payload: decodedPayload, segment: payloadsegment } = this.decodeThirdParty(withStructure)); + case "}": // Third-party + ({ payload: decodedPayload, segment: payloadsegment } = + this.decodeThirdParty(withStructure)); break; default: @@ -265,7 +354,7 @@ export class Frame implements IFrame { } if (withStructure) { - const structure: PacketStructure = []; + const structure: Dissected = []; const routingSection = this.getRoutingSection(); if (routingSection) { structure.push(routingSection); @@ -279,14 +368,17 @@ export class Frame implements IFrame { return decodedPayload; } - private decodePosition(dataType: string, withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } { + private decodePosition( + dataType: string, + withStructure: boolean = false, + ): { payload: Payload | null; segment?: Segment[] } { try { - const hasTimestamp = dataType === '/' || dataType === '@'; - const messaging = dataType === '=' || dataType === '@'; + const hasTimestamp = dataType === "/" || dataType === "@"; + const messaging = dataType === "=" || dataType === "@"; let offset = 1; // Skip data type identifier // Build structure as we parse - const structure: PacketSegment[] = withStructure ? [] : []; + const structure: Segment[] = withStructure ? [] : []; let timestamp: Timestamp | undefined = undefined; @@ -295,7 +387,8 @@ export class Frame implements IFrame { if (this.payload.length < 8) return { payload: null }; const timestampOffset = offset; const timeStr = this.payload.substring(offset, offset + 7); - const { timestamp: parsedTimestamp, segment: timestampSegment } = this.parseTimestamp(timeStr, withStructure, timestampOffset); + const { timestamp: parsedTimestamp, segment: timestampSegment } = + this.parseTimestamp(timeStr, withStructure, timestampOffset); timestamp = parsedTimestamp; if (timestampSegment) { @@ -309,14 +402,21 @@ export class Frame implements IFrame { // Check if compressed format const positionOffset = offset; - const isCompressed = this.isCompressedPosition(this.payload.substring(offset)); + const isCompressed = this.isCompressedPosition( + this.payload.substring(offset), + ); let position: Position; - let comment = ''; + let comment = ""; if (isCompressed) { // Compressed format: /YYYYXXXX$csT - const { position: compressed, segment: compressedSegment } = this.parseCompressedPosition(this.payload.substring(offset), withStructure, positionOffset); + const { position: compressed, segment: compressedSegment } = + this.parseCompressedPosition( + this.payload.substring(offset), + withStructure, + positionOffset, + ); if (!compressed) return { payload: null }; position = new Position({ @@ -337,7 +437,12 @@ export class Frame implements IFrame { comment = this.payload.substring(offset); } else { // Uncompressed format: DDMMmmH/DDDMMmmH$ - const { position: uncompressed, segment: uncompressedSegment } = this.parseUncompressedPosition(this.payload.substring(offset), withStructure, positionOffset); + const { position: uncompressed, segment: uncompressedSegment } = + this.parseUncompressedPosition( + this.payload.substring(offset), + withStructure, + positionOffset, + ); if (!uncompressed) return { payload: null }; position = new Position({ @@ -370,17 +475,18 @@ export class Frame implements IFrame { // Emit comment section as we parse if (withStructure) { structure.push({ - name: 'comment', - data: new TextEncoder().encode(comment), + name: "comment", + data: new TextEncoder().encode(comment).buffer, + isString: true, fields: [ - { type: FieldType.STRING, name: 'text', size: comment.length }, + { type: FieldType.STRING, name: "text", length: comment.length }, ], }); } } const payload: PositionPayload = { - type: 'position', + type: "position", timestamp, position, messaging, @@ -391,88 +497,100 @@ export class Frame implements IFrame { } return { payload }; - } catch (e) { + } catch { return { payload: null }; } } - private parseTimestamp(timeStr: string, withStructure: boolean = false, offset: number = 0): { timestamp: Timestamp | undefined; segment?: PacketSegment } { + private parseTimestamp( + timeStr: string, + withStructure: boolean = false, + ): { timestamp: Timestamp | undefined; segment?: Segment } { if (timeStr.length !== 7) return { timestamp: undefined }; const timeType = timeStr.charAt(6); - if (timeType === 'z') { + if (timeType === "z") { // DHM format: Day-Hour-Minute (UTC) const timestamp = new Timestamp( parseInt(timeStr.substring(2, 4), 10), parseInt(timeStr.substring(4, 6), 10), - 'DHM', + "DHM", { day: parseInt(timeStr.substring(0, 2), 10), zulu: true, - } + }, ); - const segment = withStructure ? { - name: 'timestamp', - data: new TextEncoder().encode(timeStr), - fields: [ - { type: FieldType.STRING, name: 'day (DD)', size: 2 }, - { type: FieldType.STRING, name: 'hour (HH)', size: 2 }, - { type: FieldType.STRING, name: 'minute (MM)', size: 2 }, - { type: FieldType.CHAR, name: 'timezone indicator', size: 1 }, - ], - } : undefined; + const segment = withStructure + ? { + name: "timestamp", + data: new TextEncoder().encode(timeStr).buffer, + isString: true, + fields: [ + { type: FieldType.STRING, name: "day (DD)", length: 2 }, + { type: FieldType.STRING, name: "hour (HH)", length: 2 }, + { type: FieldType.STRING, name: "minute (MM)", length: 2 }, + { type: FieldType.CHAR, name: "timezone indicator", length: 1 }, + ], + } + : undefined; return { timestamp, segment }; - } else if (timeType === 'h') { + } else if (timeType === "h") { // HMS format: Hour-Minute-Second (UTC) const timestamp = new Timestamp( parseInt(timeStr.substring(0, 2), 10), parseInt(timeStr.substring(2, 4), 10), - 'HMS', + "HMS", { seconds: parseInt(timeStr.substring(4, 6), 10), zulu: true, - } + }, ); - const segment = withStructure ? { - name: 'timestamp', - data: new TextEncoder().encode(timeStr), - fields: [ - { type: FieldType.STRING, name: 'hour (HH)', size: 2 }, - { type: FieldType.STRING, name: 'minute (MM)', size: 2 }, - { type: FieldType.STRING, name: 'second (SS)', size: 2 }, - { type: FieldType.CHAR, name: 'timezone indicator', size: 1 }, - ], - } : undefined; + const segment = withStructure + ? { + name: "timestamp", + data: new TextEncoder().encode(timeStr).buffer, + isString: true, + fields: [ + { type: FieldType.STRING, name: "hour (HH)", length: 2 }, + { type: FieldType.STRING, name: "minute (MM)", length: 2 }, + { type: FieldType.STRING, name: "second (SS)", length: 2 }, + { type: FieldType.CHAR, name: "timezone indicator", length: 1 }, + ], + } + : undefined; return { timestamp, segment }; - } else if (timeType === '/') { + } else if (timeType === "/") { // MDHM format: Month-Day-Hour-Minute (local) const timestamp = new Timestamp( parseInt(timeStr.substring(4, 6), 10), parseInt(timeStr.substring(6, 8), 10), - 'MDHM', + "MDHM", { month: parseInt(timeStr.substring(0, 2), 10), day: parseInt(timeStr.substring(2, 4), 10), zulu: false, - } + }, ); - const segment = withStructure ? { - name: 'timestamp', - data: new TextEncoder().encode(timeStr), - fields: [ - { type: FieldType.STRING, name: 'month (MM)', size: 2 }, - { type: FieldType.STRING, name: 'day (DD)', size: 2 }, - { type: FieldType.STRING, name: 'hour (HH)', size: 2 }, - { type: FieldType.STRING, name: 'minute (MM)', size: 2 }, - { type: FieldType.CHAR, name: 'timezone indicator', size: 1 }, - ], - } : undefined; + const segment = withStructure + ? { + name: "timestamp", + data: new TextEncoder().encode(timeStr).buffer, + isString: true, + fields: [ + { type: FieldType.STRING, name: "month (MM)", length: 2 }, + { type: FieldType.STRING, name: "day (DD)", length: 2 }, + { type: FieldType.STRING, name: "hour (HH)", length: 2 }, + { type: FieldType.STRING, name: "minute (MM)", length: 2 }, + { type: FieldType.CHAR, name: "timezone indicator", length: 1 }, + ], + } + : undefined; return { timestamp, segment }; } @@ -487,7 +605,7 @@ export class Frame implements IFrame { // Uncompressed APRS positions do not have a fixed symbol table separator; // position 8 is a symbol table identifier and may vary. if (data.length >= 19) { - const uncompressed = this.parseUncompressedPosition(data, false, 0); + const uncompressed = this.parseUncompressedPosition(data, false); if (uncompressed.position) { return false; } @@ -501,13 +619,25 @@ export class Frame implements IFrame { const lon1 = data.charCodeAt(5); const lon2 = data.charCodeAt(6); - return lat1 >= 33 && lat1 <= 124 && - lat2 >= 33 && lat2 <= 124 && - lon1 >= 33 && lon1 <= 124 && - lon2 >= 33 && lon2 <= 124; + return ( + lat1 >= 33 && + lat1 <= 124 && + lat2 >= 33 && + lat2 <= 124 && + lon1 >= 33 && + lon1 <= 124 && + lon2 >= 33 && + lon2 <= 124 + ); } - private parseCompressedPosition(data: string, withStructure: boolean = false, offset: number = 0): { position: { latitude: number; longitude: number; symbol: any; altitude?: number } | null; segment?: PacketSegment } { + private parseCompressedPosition( + data: string, + withStructure: boolean = false, + ): { + position: IPosition | null; + segment?: Segment; + } { if (data.length < 13) return { position: null }; const symbolTable = data.charAt(0); @@ -523,10 +653,10 @@ export class Frame implements IFrame { const lonBase91 = base91ToNumber(lonStr); // Convert to degrees - const latitude = 90 - (latBase91 / 380926); - const longitude = -180 + (lonBase91 / 190463); + const latitude = 90 - latBase91 / 380926; + const longitude = -180 + lonBase91 / 190463; - const result: any = { + const result: IPosition = { latitude, longitude, symbol: { @@ -539,33 +669,42 @@ export class Frame implements IFrame { const cs = data.charAt(10); const t = data.charCodeAt(11); - if (cs === ' ' && t >= 33 && t <= 124) { + if (cs === " " && t >= 33 && t <= 124) { // Compressed altitude: altitude = 1.002^(t-33) feet const altFeet = Math.pow(1.002, t - 33); result.altitude = altFeet * 0.3048; // Convert to meters } - const section: PacketSegment | undefined = withStructure ? { - name: 'position', - data: new TextEncoder().encode(data.substring(0, 13)), - fields: [ - { type: FieldType.CHAR, size: 1, name: 'symbol table' }, - { type: FieldType.STRING, size: 4, name: 'latitude' }, - { type: FieldType.STRING, size: 4, name: 'longitude' }, - { type: FieldType.CHAR, size: 1, name: 'symbol code' }, - { type: FieldType.CHAR, size: 1, name: 'course/speed type' }, - { type: FieldType.CHAR, size: 1, name: 'course/speed value' }, - { type: FieldType.CHAR, size: 1, name: 'altitude' }, - ], - } : undefined; + const section: Segment | undefined = withStructure + ? { + name: "position", + data: new TextEncoder().encode(data.substring(0, 13)).buffer, + isString: true, + fields: [ + { type: FieldType.CHAR, length: 1, name: "symbol table" }, + { type: FieldType.STRING, length: 4, name: "latitude" }, + { type: FieldType.STRING, length: 4, name: "longitude" }, + { type: FieldType.CHAR, length: 1, name: "symbol code" }, + { type: FieldType.CHAR, length: 1, name: "course/speed type" }, + { type: FieldType.CHAR, length: 1, name: "course/speed value" }, + { type: FieldType.CHAR, length: 1, name: "altitude" }, + ], + } + : undefined; return { position: result, segment: section }; - } catch (e) { + } catch { return { position: null }; } } - private parseUncompressedPosition(data: string, withStructure: boolean = false, offset: number = 0): { position: { latitude: number; longitude: number; symbol: any; ambiguity?: number } | null; segment?: PacketSegment } { + private parseUncompressedPosition( + data: string, + withStructure: boolean = false, + ): { + position: IPosition | null; + segment?: Segment; + } { if (data.length < 19) return { position: null }; // Format: DDMMmmH/DDDMMmmH$ where H is hemisphere, $ is symbol code @@ -588,8 +727,8 @@ export class Frame implements IFrame { } // Replace spaces with zeros for parsing - const latStrNormalized = latStr.replace(/ /g, '0'); - const lonStrNormalized = lonStr.replace(/ /g, '0'); + const latStrNormalized = latStr.replace(/ /g, "0"); + const lonStrNormalized = lonStr.replace(/ /g, "0"); // Parse latitude const latDeg = parseInt(latStrNormalized.substring(0, 2), 10); @@ -597,10 +736,10 @@ export class Frame implements IFrame { const latHem = latStrNormalized.charAt(7); if (isNaN(latDeg) || isNaN(latMin)) return { position: null }; - if (latHem !== 'N' && latHem !== 'S') return { position: null }; + if (latHem !== "N" && latHem !== "S") return { position: null }; - let latitude = latDeg + (latMin / 60); - if (latHem === 'S') latitude = -latitude; + let latitude = latDeg + latMin / 60; + if (latHem === "S") latitude = -latitude; // Parse longitude const lonDeg = parseInt(lonStrNormalized.substring(0, 3), 10); @@ -608,12 +747,12 @@ export class Frame implements IFrame { const lonHem = lonStrNormalized.charAt(8); if (isNaN(lonDeg) || isNaN(lonMin)) return { position: null }; - if (lonHem !== 'E' && lonHem !== 'W') return { position: null }; + if (lonHem !== "E" && lonHem !== "W") return { position: null }; - let longitude = lonDeg + (lonMin / 60); - if (lonHem === 'W') longitude = -longitude; + let longitude = lonDeg + lonMin / 60; + if (lonHem === "W") longitude = -longitude; - const result: any = { + const result: IPosition = { latitude, longitude, symbol: { @@ -626,36 +765,57 @@ export class Frame implements IFrame { result.ambiguity = ambiguity; } - const segment: PacketSegment | undefined = withStructure ? { - name: 'position', - data: new TextEncoder().encode(data.substring(0, 19)), - fields: [ - { type: FieldType.STRING, size: 8, name: 'latitude' }, - { type: FieldType.CHAR, size: 1, name: 'symbol table' }, - { type: FieldType.STRING, size: 9, name: 'longitude' }, - { type: FieldType.CHAR, size: 1, name: 'symbol code' }, - ], - } : undefined; + const segment: Segment | undefined = withStructure + ? { + name: "position", + data: new TextEncoder().encode(data.substring(0, 19)).buffer, + isString: true, + fields: [ + { type: FieldType.STRING, length: 8, name: "latitude" }, + { type: FieldType.CHAR, length: 1, name: "symbol table" }, + { type: FieldType.STRING, length: 9, name: "longitude" }, + { type: FieldType.CHAR, length: 1, name: "symbol code" }, + ], + } + : undefined; return { position: result, segment }; } - private decodeMicE(withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } { + private decodeMicE(withStructure: boolean = false): { + payload: Payload | null; + segment?: Segment[]; + } { try { - // TODO: Add section emission support when withStructure is true - // For now, Mic-E returns payload without structure - // Mic-E encodes position in both destination address and information field const dest = this.destination.call; if (dest.length < 6) return { payload: null }; if (this.payload.length < 9) return { payload: null }; // Need at least data type + 8 bytes + const segments: Segment[] = withStructure ? [] : []; + // Decode latitude from destination address (6 characters) const latResult = this.decodeMicELatitude(dest); if (!latResult) return { payload: null }; - const { latitude, messageType, longitudeOffset, isWest, isStandard } = latResult; + const { latitude, messageType, longitudeOffset, isWest, isStandard } = + latResult; + + if (withStructure) { + segments.push({ + name: "mic-e destination", + data: new TextEncoder().encode(dest).buffer, + isString: true, + fields: [ + { + type: FieldType.STRING, + name: "destination", + length: dest.length, + }, + ], + }); + } // Parse information field (skip data type identifier at position 0) let offset = 1; @@ -677,7 +837,7 @@ export class Frame implements IFrame { lonDeg -= 190; } - let longitude = lonDeg + (lonMinRaw / 60.0) + (lonHunRaw / 6000.0); + let longitude = lonDeg + lonMinRaw / 60.0 + lonHunRaw / 6000.0; if (isWest) { longitude = -longitude; } @@ -688,8 +848,8 @@ export class Frame implements IFrame { const se = this.payload.charCodeAt(offset + 2) - 28; offset += 3; - let speed = (sp * 10) + Math.floor(dc / 10); // Speed in knots - let course = ((dc % 10) * 100) + se; // Course in degrees + let speed = sp * 10 + Math.floor(dc / 10); // Speed in knots + let course = (dc % 10) * 100 + se; // Course in degrees if (course >= 400) course -= 400; if (speed >= 800) speed -= 800; @@ -706,29 +866,26 @@ export class Frame implements IFrame { // Parse remaining data (altitude, comment, telemetry) const remaining = this.payload.substring(offset); let altitude: number | undefined = undefined; - let comment = remaining; + const comment = remaining; // Check for altitude in various formats - // Format 1: }xyz where xyz is altitude in base-91 (obsolete) - // Format 2: /A=NNNNNN where NNNNNN is altitude in feet const altMatch = remaining.match(/\/A=(\d{6})/); if (altMatch) { altitude = parseInt(altMatch[1], 10) * 0.3048; // feet to meters - } else if (remaining.startsWith('}')) { - // Base-91 altitude (3 characters after }) + } else if (remaining.startsWith("}")) { if (remaining.length >= 4) { try { const altBase91 = remaining.substring(1, 4); const altFeet = base91ToNumber(altBase91) - 10000; altitude = altFeet * 0.3048; // feet to meters - } catch (e) { + } catch { // Ignore altitude parsing errors } } } - const result: any = { - type: 'position', + const result: PositionPayload = { + type: "position", position: { latitude, longitude, @@ -760,8 +917,41 @@ export class Frame implements IFrame { result.position.comment = comment; } + if (withStructure) { + // Information field section (bytes after data type up to comment) + const infoData = this.payload.substring(1, offset); + segments.push({ + name: "mic-e info", + data: new TextEncoder().encode(infoData).buffer, + isString: true, + fields: [ + { type: FieldType.CHAR, name: "longitude deg", length: 1 }, + { type: FieldType.CHAR, name: "longitude min", length: 1 }, + { type: FieldType.CHAR, name: "longitude hundredths", length: 1 }, + { type: FieldType.CHAR, name: "speed byte", length: 1 }, + { type: FieldType.CHAR, name: "course byte 1", length: 1 }, + { type: FieldType.CHAR, name: "course byte 2", length: 1 }, + { type: FieldType.CHAR, name: "symbol code", length: 1 }, + { type: FieldType.CHAR, name: "symbol table", length: 1 }, + ], + }); + + if (comment && comment.length > 0) { + segments.push({ + name: "comment", + data: new TextEncoder().encode(comment).buffer, + isString: true, + fields: [ + { type: FieldType.STRING, name: "text", length: comment.length }, + ], + }); + } + + return { payload: result, segment: segments }; + } + return { payload: result }; - } catch (e) { + } catch { return { payload: null }; } } @@ -821,7 +1011,7 @@ export class Frame implements IFrame { const latMin = digits[2] * 10 + digits[3]; const latHun = digits[4] * 10 + digits[5]; - let latitude = latDeg + (latMin / 60.0) + (latHun / 6000.0); + let latitude = latDeg + latMin / 60.0 + latHun / 6000.0; // Message bits determine hemisphere and other flags // Bit 3 (messageBits[3]): 0 = North, 1 = South @@ -838,16 +1028,16 @@ export class Frame implements IFrame { // Decode message type from bits 0, 1, 2 const msgValue = messageBits[0] * 4 + messageBits[1] * 2 + messageBits[2]; const messageTypes = [ - 'M0: Off Duty', - 'M1: En Route', - 'M2: In Service', - 'M3: Returning', - 'M4: Committed', - 'M5: Special', - 'M6: Priority', - 'M7: Emergency', + "M0: Off Duty", + "M1: En Route", + "M2: In Service", + "M3: Returning", + "M4: Committed", + "M5: Special", + "M6: Priority", + "M7: Emergency", ]; - const messageType = messageTypes[msgValue] || 'Unknown'; + const messageType = messageTypes[msgValue] || "Unknown"; // Standard vs custom message indicator const isStandard = messageBits[0] === 1; @@ -861,20 +1051,26 @@ export class Frame implements IFrame { }; } - private decodeMessage(withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } { + private decodeMessage(withStructure: boolean = false): { + payload: Payload | null; + segment?: Segment[]; + } { // Message format: :AAAAAAAAA[ ]:message text // where AAAAAAAAA is a 9-character recipient field (padded with spaces) if (this.payload.length < 2) return { payload: null }; let offset = 1; // skip ':' data type - const segments: PacketSegment[] = withStructure ? [] : []; + const segments: Segment[] = withStructure ? [] : []; // Attempt to read a 9-char recipient field if present - let recipient = ''; + let recipient = ""; if (this.payload.length >= offset + 1) { // Try to read up to 9 chars for recipient, but stop early if a ':' separator appears - const look = this.payload.substring(offset, Math.min(offset + 9, this.payload.length)); - const sepIdx = look.indexOf(':'); + const look = this.payload.substring( + offset, + Math.min(offset + 9, this.payload.length), + ); + const sepIdx = look.indexOf(":"); let raw = look; if (sepIdx !== -1) { raw = look.substring(0, sepIdx); @@ -888,18 +1084,17 @@ export class Frame implements IFrame { recipient = raw.trimEnd(); if (withStructure) { segments.push({ - name: 'recipient', - data: new TextEncoder().encode(raw), - fields: [ - { type: FieldType.STRING, name: 'to', size: 9 }, - ], + name: "recipient", + data: new TextEncoder().encode(raw).buffer, + isString: true, + fields: [{ type: FieldType.STRING, name: "to", length: 9 }], }); } // Advance offset past the raw we consumed offset += raw.length; // If there was a ':' immediately after the consumed raw, skip it as separator - if (this.payload.charAt(offset) === ':') { + if (this.payload.charAt(offset) === ":") { offset += 1; } else if (sepIdx !== -1) { // Shouldn't normally happen, but ensure we advance past separator @@ -909,14 +1104,18 @@ export class Frame implements IFrame { // After recipient there is typically a space and a colon separator before the text // Find the first ':' after the recipient (it separates the address field from the text) - let textStart = this.payload.indexOf(':', offset); + let textStart = this.payload.indexOf(":", offset); if (textStart === -1) { // No explicit separator; skip any spaces and take remainder as text - while (this.payload.charAt(offset) === ' ' && offset < this.payload.length) offset += 1; + while ( + this.payload.charAt(offset) === " " && + offset < this.payload.length + ) + offset += 1; textStart = offset - 1; } - let text = ''; + let text = ""; if (textStart >= 0 && textStart + 1 <= this.payload.length) { text = this.payload.substring(textStart + 1); } @@ -924,32 +1123,34 @@ export class Frame implements IFrame { if (withStructure) { // Emit text section segments.push({ - name: 'text', - data: new TextEncoder().encode(text), - fields: [ - { type: FieldType.STRING, name: 'text', size: text.length }, - ], + name: "text", + data: new TextEncoder().encode(text).buffer, + isString: true, + fields: [{ type: FieldType.STRING, name: "text", length: text.length }], }); - const payload: any = { - type: 'message', - to: recipient || undefined, + const payload: MessagePayload = { + type: "message", + addressee: recipient, text, }; return { payload, segment: segments }; } - const payload: any = { - type: 'message', - to: recipient || undefined, + const payload: MessagePayload = { + type: "message", + addressee: recipient, text, }; return { payload }; } - private decodeObject(withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } { + private decodeObject(withStructure: boolean = false): { + payload: Payload | null; + segment?: Segment[]; + } { try { // Object format: ;AAAAAAAAAcDDHHMMzDDMM.hhN/DDDMM.hhW$comment // ^ data type @@ -958,39 +1159,46 @@ export class Frame implements IFrame { if (this.payload.length < 18) return { payload: null }; // 1 + 9 + 1 + 7 minimum let offset = 1; // Skip data type identifier ';' - const segment: PacketSegment[] = withStructure ? [] : []; + const segment: Segment[] = withStructure ? [] : []; const rawName = this.payload.substring(offset, offset + 9); const name = rawName.trimEnd(); if (withStructure) { segment.push({ - name: 'object name', - data: new TextEncoder().encode(rawName), - fields: [ - { type: FieldType.STRING, name: 'name', size: 9 }, - ], + name: "object name", + data: new TextEncoder().encode(rawName).buffer, + isString: true, + fields: [{ type: FieldType.STRING, name: "name", length: 9 }], }); } offset += 9; const stateChar = this.payload.charAt(offset); - if (stateChar !== '*' && stateChar !== '_') { + if (stateChar !== "*" && stateChar !== "_") { return { payload: null }; } - const alive = stateChar === '*'; + const alive = stateChar === "*"; if (withStructure) { segment.push({ - name: 'object state', - data: new TextEncoder().encode(stateChar), + name: "object state", + data: new TextEncoder().encode(stateChar).buffer, + isString: true, fields: [ - { type: FieldType.CHAR, name: 'State (* alive, _ killed)', size: 1 }, + { + type: FieldType.CHAR, + name: "State (* alive, _ killed)", + length: 1, + }, ], }); } offset += 1; const timeStr = this.payload.substring(offset, offset + 7); - const { timestamp, segment: timestampSection } = this.parseTimestamp(timeStr, withStructure, offset); + const { timestamp, segment: timestampSection } = this.parseTimestamp( + timeStr, + withStructure, + ); if (!timestamp) { return { payload: null }; } @@ -999,14 +1207,19 @@ export class Frame implements IFrame { } offset += 7; - const positionOffset = offset; - const isCompressed = this.isCompressedPosition(this.payload.substring(offset)); + const isCompressed = this.isCompressedPosition( + this.payload.substring(offset), + ); - let position: { latitude: number; longitude: number; symbol: any; ambiguity?: number; altitude?: number; comment?: string } | null = null; + let position: IPosition | null = null; let consumed = 0; if (isCompressed) { - const { position: compressed, segment: compressedSection } = this.parseCompressedPosition(this.payload.substring(offset), withStructure, positionOffset); + const { position: compressed, segment: compressedSection } = + this.parseCompressedPosition( + this.payload.substring(offset), + withStructure, + ); if (!compressed) return { payload: null }; position = { @@ -1021,7 +1234,11 @@ export class Frame implements IFrame { segment.push(compressedSection); } } else { - const { position: uncompressed, segment: uncompressedSection } = this.parseUncompressedPosition(this.payload.substring(offset), withStructure, positionOffset); + const { position: uncompressed, segment: uncompressedSection } = + this.parseUncompressedPosition( + this.payload.substring(offset), + withStructure, + ); if (!uncompressed) return { payload: null }; position = { @@ -1044,17 +1261,18 @@ export class Frame implements IFrame { if (withStructure) { segment.push({ - name: 'Comment', - data: new TextEncoder().encode(comment), + name: "Comment", + data: new TextEncoder().encode(comment).buffer, + isString: true, fields: [ - { type: FieldType.STRING, name: 'text', size: comment.length }, + { type: FieldType.STRING, name: "text", length: comment.length }, ], }); } } - const payload: any = { - type: 'object', + const payload: ObjectPayload = { + type: "object", name, timestamp, alive, @@ -1066,45 +1284,52 @@ export class Frame implements IFrame { } return { payload }; - } catch (e) { + } catch { return { payload: null }; } } - private decodeItem(withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } { + private decodeItem(withStructure: boolean = false): { + payload: Payload | null; + segment?: Segment[]; + } { // Item format is similar to Object but name may be 3-9 chars (stored in a 9-char field) // Example: )NNN... where ) is data type, next 9 chars are name, then state char, then timestamp, then position if (this.payload.length < 12) return { payload: null }; // minimal: 1 + 3 + 1 + 7 let offset = 1; // skip data type identifier ')' - const segment: PacketSegment[] = withStructure ? [] : []; + const segment: Segment[] = withStructure ? [] : []; // Read 9-char name field (pad/truncate as present) const rawName = this.payload.substring(offset, offset + 9); const name = rawName.trimEnd(); if (withStructure) { segment.push({ - name: 'item name', - data: new TextEncoder().encode(rawName), - fields: [ - { type: FieldType.STRING, name: 'name', size: 9 }, - ], + name: "item name", + data: new TextEncoder().encode(rawName).buffer, + isString: true, + fields: [{ type: FieldType.STRING, name: "name", length: 9 }], }); } offset += 9; // State character: '*' = alive, '_' = killed const stateChar = this.payload.charAt(offset); - if (stateChar !== '*' && stateChar !== '_') { + if (stateChar !== "*" && stateChar !== "_") { return { payload: null }; } - const alive = stateChar === '*'; + const alive = stateChar === "*"; if (withStructure) { segment.push({ - name: 'item state', - data: new TextEncoder().encode(stateChar), + name: "item state", + data: new TextEncoder().encode(stateChar).buffer, + isString: true, fields: [ - { type: FieldType.CHAR, name: 'State (* alive, _ killed)', size: 1 }, + { + type: FieldType.CHAR, + name: "State (* alive, _ killed)", + length: 1, + }, ], }); } @@ -1112,19 +1337,29 @@ export class Frame implements IFrame { // Timestamp (7 chars) const timeStr = this.payload.substring(offset, offset + 7); - const { timestamp, segment: timestampSection } = this.parseTimestamp(timeStr, withStructure, offset); + const { timestamp, segment: timestampSection } = this.parseTimestamp( + timeStr.substring(offset), + withStructure, + ); if (!timestamp) return { payload: null }; if (timestampSection) segment.push(timestampSection); offset += 7; - const positionOffset = offset; - const isCompressed = this.isCompressedPosition(this.payload.substring(offset)); + const isCompressed = this.isCompressedPosition( + this.payload.substring(offset), + ); - let position: { latitude: number; longitude: number; symbol: any; ambiguity?: number; altitude?: number; comment?: string } | null = null; + // eslint-disable-next-line no-useless-assignment + let position: IPosition | null = null; + // eslint-disable-next-line no-useless-assignment let consumed = 0; if (isCompressed) { - const { position: compressed, segment: compressedSection } = this.parseCompressedPosition(this.payload.substring(offset), withStructure, positionOffset); + const { position: compressed, segment: compressedSection } = + this.parseCompressedPosition( + this.payload.substring(offset), + withStructure, + ); if (!compressed) return { payload: null }; position = { @@ -1137,7 +1372,11 @@ export class Frame implements IFrame { if (compressedSection) segment.push(compressedSection); } else { - const { position: uncompressed, segment: uncompressedSection } = this.parseUncompressedPosition(this.payload.substring(offset), withStructure, positionOffset); + const { position: uncompressed, segment: uncompressedSection } = + this.parseUncompressedPosition( + this.payload.substring(offset), + withStructure, + ); if (!uncompressed) return { payload: null }; position = { @@ -1157,17 +1396,18 @@ export class Frame implements IFrame { position.comment = comment; if (withStructure) { segment.push({ - name: 'Comment', - data: new TextEncoder().encode(comment), + name: "Comment", + data: new TextEncoder().encode(comment).buffer, + isString: true, fields: [ - { type: FieldType.STRING, name: 'text', size: comment.length }, + { type: FieldType.STRING, name: "text", length: comment.length }, ], }); } } - const payload: any = { - type: 'item', + const payload: ItemPayload = { + type: "item", name, alive, position, @@ -1180,19 +1420,26 @@ export class Frame implements IFrame { return { payload }; } - private decodeStatus(withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } { + private decodeStatus(withStructure: boolean = false): { + payload: Payload | null; + segment?: Segment[]; + } { // Status payload: optional 7-char timestamp followed by free text. // We'll also detect a trailing Maidenhead locator (4 or 6 chars) and expose it. const offsetBase = 1; // skip data type identifier '>' if (this.payload.length <= offsetBase) return { payload: null }; let offset = offsetBase; - const segments: PacketSegment[] = withStructure ? [] : []; + const segments: Segment[] = withStructure ? [] : []; // Try parse optional timestamp (7 chars) if (this.payload.length >= offset + 7) { const timeStr = this.payload.substring(offset, offset + 7); - const { timestamp, segment: tsSegment } = this.parseTimestamp(timeStr, withStructure, offset); + const { timestamp, segment: tsSegment } = this.parseTimestamp( + timeStr, + withStructure, + offset, + ); if (timestamp) { offset += 7; if (tsSegment) segments.push(tsSegment); @@ -1212,8 +1459,8 @@ export class Frame implements IFrame { statusText = text.slice(0, mhMatch.index).trimEnd(); } - const payload: any = { - type: 'status', + const payload: StatusPayload = { + type: "status", timestamp: undefined, text: statusText, }; @@ -1222,7 +1469,7 @@ export class Frame implements IFrame { if (segments.length > 0) { // The first segment may be timestamp; parseTimestamp returns the Timestamp object // Re-parse to obtain timestamp object (cheap) - alternate would be to capture earlier - const timeSegment = segments.find(s => s.name === 'timestamp'); + const timeSegment = segments.find((s) => s.name === "timestamp"); if (timeSegment) { const tsStr = new TextDecoder().decode(timeSegment.data); const { timestamp } = this.parseTimestamp(tsStr, false, 0); @@ -1234,11 +1481,10 @@ export class Frame implements IFrame { if (withStructure) { segments.push({ - name: 'status', - data: new TextEncoder().encode(text), - fields: [ - { type: FieldType.STRING, name: 'text', size: text.length }, - ], + name: "status", + data: new TextEncoder().encode(text).buffer, + isString: true, + fields: [{ type: FieldType.STRING, name: "text", length: text.length }], }); return { payload, segment: segments }; } @@ -1246,39 +1492,60 @@ export class Frame implements IFrame { return { payload }; } - private decodeQuery(withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } { + private decodeQuery(withStructure: boolean = false): { + payload: Payload | null; + segment?: Segment[]; + } { // TODO: Implement query decoding with section emission - return { payload: null }; + return { payload: withStructure ? null : null }; } - private decodeTelemetry(withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } { + private decodeTelemetry(withStructure: boolean = false): { + payload: Payload | null; + segment?: Segment[]; + } { // TODO: Implement telemetry decoding with section emission - return { payload: null }; + return { payload: withStructure ? null : null }; } - private decodeWeather(withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } { + private decodeWeather(withStructure: boolean = false): { + payload: Payload | null; + segment?: Segment[]; + } { // TODO: Implement weather decoding with section emission - return { payload: null }; + return { payload: withStructure ? null : null }; } - private decodeRawGPS(withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } { + private decodeRawGPS(withStructure: boolean = false): { + payload: Payload | null; + segment?: Segment[]; + } { // TODO: Implement raw GPS decoding with section emission - return { payload: null }; + return { payload: withStructure ? null : null }; } - private decodeCapabilities(withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } { + private decodeCapabilities(withStructure: boolean = false): { + payload: Payload | null; + segment?: Segment[]; + } { // TODO: Implement capabilities decoding with section emission - return { payload: null }; + return { payload: withStructure ? null : null }; } - private decodeUserDefined(withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } { + private decodeUserDefined(withStructure: boolean = false): { + payload: Payload | null; + segment?: Segment[]; + } { // TODO: Implement user-defined decoding with section emission - return { payload: null }; + return { payload: withStructure ? null : null }; } - private decodeThirdParty(withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } { + private decodeThirdParty(withStructure: boolean = false): { + payload: Payload | null; + segment?: Segment[]; + } { // TODO: Implement third-party decoding with section emission - return { payload: null }; + return { payload: withStructure ? null : null }; } public static fromString(data: string): Frame { @@ -1293,64 +1560,66 @@ export class Frame implements IFrame { const parseFrame = (data: string): Frame => { const encoder = new TextEncoder(); - const routeSepIndex = data.indexOf(':'); + const routeSepIndex = data.indexOf(":"); if (routeSepIndex === -1) { - throw new Error('APRS: invalid frame, no route separator found'); + throw new Error("APRS: invalid frame, no route separator found"); } const route = data.slice(0, routeSepIndex); const payload = data.slice(routeSepIndex + 1); - const parts = route.split('>'); + const parts = route.split(">"); if (parts.length < 2) { - throw new Error('APRS: invalid addresses in route'); + throw new Error("APRS: invalid addresses in route"); } // Parse source - track byte offset as we parse - let offset = 0; const sourceStr = parts[0]; const source = Address.fromString(sourceStr); - offset += sourceStr.length + 1; // +1 for '>' - // Parse destination and path - const destinationAndPath = parts[1].split(','); + const destinationAndPath = parts[1].split(","); const destinationStr = destinationAndPath[0]; const destination = Address.fromString(destinationStr); - offset += destinationStr.length; - // Parse path const path: Address[] = []; - const pathFields: PacketField[] = []; + const pathFields: Field[] = []; for (let i = 1; i < destinationAndPath.length; i++) { - offset += 1; // +1 for ',' const pathStr = destinationAndPath[i]; path.push(Address.fromString(pathStr)); pathFields.push({ type: FieldType.CHAR, name: `Path separator ${i}`, - size: 1 + length: 1, }); pathFields.push({ type: FieldType.STRING, name: `Repeater ${i}`, - size: pathStr.length, + length: pathStr.length, }); - offset += pathStr.length; } - const routingSection: PacketSegment = { - name: 'Routing', - data: encoder.encode(data.slice(0, routeSepIndex)), + const routingSection: Segment = { + name: "Routing", + data: encoder.encode(data.slice(0, routeSepIndex)).buffer, + isString: true, fields: [ - { type: FieldType.STRING, name: 'Source address', size: sourceStr.length }, - { type: FieldType.CHAR, name: 'Route separator', size: 1 }, - { type: FieldType.STRING, name: 'Destination address', size: destinationStr.length }, + { + type: FieldType.STRING, + name: "Source address", + length: sourceStr.length, + }, + { type: FieldType.CHAR, name: "Route separator", length: 1 }, + { + type: FieldType.STRING, + name: "Destination address", + length: destinationStr.length, + }, ...pathFields, - { type: FieldType.CHAR, name: 'Payload separator', size: 1 }, + { type: FieldType.CHAR, name: "Payload separator", length: 1 }, ], }; return new Frame(source, destination, path, payload, routingSection); -} +}; diff --git a/src/frame.types.ts b/src/frame.types.ts index 399b688..1fddbff 100644 --- a/src/frame.types.ts +++ b/src/frame.types.ts @@ -1,82 +1,83 @@ import { PacketSegment, PacketStructure } from "./parser.types"; export interface IAddress { - call: string; - ssid: string; + call: string; + ssid: string; isRepeated: boolean; } export interface IFrame { - source: IAddress; + source: IAddress; destination: IAddress; - path: IAddress[]; - payload: string; + path: IAddress[]; + payload: string; } // APRS Data Type Identifiers (first character of payload) export const DataTypeIdentifier = { // Position Reports - PositionNoTimestampNoMessaging: '!', - PositionNoTimestampWithMessaging: '=', - PositionWithTimestampNoMessaging: '/', - PositionWithTimestampWithMessaging: '@', + PositionNoTimestampNoMessaging: "!", + PositionNoTimestampWithMessaging: "=", + PositionWithTimestampNoMessaging: "/", + PositionWithTimestampWithMessaging: "@", // Mic-E - MicECurrent: '`', + MicECurrent: "`", MicEOld: "'", // Messages and Bulletins - Message: ':', + Message: ":", // Objects and Items - Object: ';', - Item: ')', + Object: ";", + Item: ")", // Status - Status: '>', + Status: ">", // Query - Query: '?', + Query: "?", // Telemetry - TelemetryData: 'T', + TelemetryData: "T", // Weather - WeatherReportNoPosition: '_', + WeatherReportNoPosition: "_", // Raw GPS Data - RawGPS: '$', + RawGPS: "$", // Station Capabilities - StationCapabilities: '<', + StationCapabilities: "<", // User-Defined - UserDefined: '{', + UserDefined: "{", // Third-Party Traffic - ThirdParty: '}', + ThirdParty: "}", // Invalid/Test Data - InvalidOrTest: ',', + InvalidOrTest: ",", } as const; -export type DataTypeIdentifier = typeof DataTypeIdentifier[keyof typeof DataTypeIdentifier]; +export type DataTypeIdentifier = + (typeof DataTypeIdentifier)[keyof typeof DataTypeIdentifier]; export interface ISymbol { - table: string; // Symbol table identifier - code: string; // Symbol code + table: string; // Symbol table identifier + code: string; // Symbol code toString(): string; // Return combined symbol representation (e.g., "tablecode") } // Position data common to multiple formats export interface IPosition { - latitude: number; // Decimal degrees - longitude: number; // Decimal degrees + latitude: number; // Decimal degrees + longitude: number; // Decimal degrees ambiguity?: number; // Position ambiguity (0-4) - altitude?: number; // Meters - speed?: number; // Speed in knots/kmh depending on source - course?: number; // Course in degrees + altitude?: number; // Meters + speed?: number; // Speed in knots/kmh depending on source + course?: number; // Course in degrees symbol?: ISymbol; comment?: string; @@ -86,22 +87,22 @@ export interface IPosition { } export interface ITimestamp { - day?: number; // Day of month (DHM format) - month?: number; // Month (MDHM format) + day?: number; // Day of month (DHM format) + month?: number; // Month (MDHM format) hours: number; minutes: number; seconds?: number; - format: 'DHM' | 'HMS' | 'MDHM'; // Day-Hour-Minute, Hour-Minute-Second, Month-Day-Hour-Minute - zulu?: boolean; // Is UTC/Zulu time - toDate(): Date; // Convert to Date object respecting timezone + format: "DHM" | "HMS" | "MDHM"; // Day-Hour-Minute, Hour-Minute-Second, Month-Day-Hour-Minute + zulu?: boolean; // Is UTC/Zulu time + toDate(): Date; // Convert to Date object respecting timezone } // Position Report Payload export interface PositionPayload { - type: 'position'; + type: "position"; timestamp?: ITimestamp; position: IPosition; - messaging: boolean; // Whether APRS messaging is enabled + messaging: boolean; // Whether APRS messaging is enabled micE?: { messageType?: string; isStandard?: boolean; @@ -117,50 +118,50 @@ export interface CompressedPosition { table: string; code: string; }; - course?: number; // Degrees - speed?: number; // Knots - range?: number; // Miles - altitude?: number; // Feet + course?: number; // Degrees + speed?: number; // Knots + range?: number; // Miles + altitude?: number; // Feet radioRange?: number; // Miles - compression: 'old' | 'current'; + compression: "old" | "current"; } // Mic-E Payload (compressed in destination address) export interface MicEPayload { - type: 'mic-e'; + type: "mic-e"; position: IPosition; course?: number; speed?: number; altitude?: number; - messageType?: string; // Standard Mic-E message - telemetry?: number[]; // Optional telemetry channels + messageType?: string; // Standard Mic-E message + telemetry?: number[]; // Optional telemetry channels status?: string; } // Message Payload export interface MessagePayload { - type: 'message'; - addressee: string; // 9 character padded callsign - text: string; // Message text - messageNumber?: string; // Message ID for acknowledgment - ack?: string; // Acknowledgment of message ID - reject?: string; // Rejection of message ID + type: "message"; + addressee: string; // 9 character padded callsign + text: string; // Message text + messageNumber?: string; // Message ID for acknowledgment + ack?: string; // Acknowledgment of message ID + reject?: string; // Rejection of message ID } // Bulletin/Announcement (variant of message) export interface BulletinPayload { - type: 'bulletin'; - bulletinId: string; // Bulletin identifier (BLN#) + type: "bulletin"; + bulletinId: string; // Bulletin identifier (BLN#) text: string; - group?: string; // Optional group bulletin + group?: string; // Optional group bulletin } // Object Payload export interface ObjectPayload { - type: 'object'; - name: string; // 9 character object name + type: "object"; + name: string; // 9 character object name timestamp: ITimestamp; - alive: boolean; // True if object is active, false if killed + alive: boolean; // True if object is active, false if killed position: IPosition; course?: number; speed?: number; @@ -168,15 +169,15 @@ export interface ObjectPayload { // Item Payload export interface ItemPayload { - type: 'item'; - name: string; // 3-9 character item name - alive: boolean; // True if item is active, false if killed + type: "item"; + name: string; // 3-9 character item name + alive: boolean; // True if item is active, false if killed position: IPosition; } // Status Payload export interface StatusPayload { - type: 'status'; + type: "status"; timestamp?: ITimestamp; text: string; maidenhead?: string; // Optional Maidenhead grid locator @@ -188,106 +189,106 @@ export interface StatusPayload { // Query Payload export interface QueryPayload { - type: 'query'; - queryType: string; // e.g., 'APRSD', 'APRST', 'PING' - target?: string; // Target callsign or area + type: "query"; + queryType: string; // e.g., 'APRSD', 'APRST', 'PING' + target?: string; // Target callsign or area } // Telemetry Data Payload export interface TelemetryDataPayload { - type: 'telemetry-data'; + type: "telemetry-data"; sequence: number; - analog: number[]; // Up to 5 analog channels - digital: number; // 8-bit digital value + analog: number[]; // Up to 5 analog channels + digital: number; // 8-bit digital value } // Telemetry Parameter Names export interface TelemetryParameterPayload { - type: 'telemetry-parameters'; - names: string[]; // Parameter names + type: "telemetry-parameters"; + names: string[]; // Parameter names } // Telemetry Unit/Label export interface TelemetryUnitPayload { - type: 'telemetry-units'; - units: string[]; // Units for each parameter + type: "telemetry-units"; + units: string[]; // Units for each parameter } // Telemetry Coefficients export interface TelemetryCoefficientsPayload { - type: 'telemetry-coefficients'; + type: "telemetry-coefficients"; coefficients: { - a: number[]; // a coefficients - b: number[]; // b coefficients - c: number[]; // c coefficients + a: number[]; // a coefficients + b: number[]; // b coefficients + c: number[]; // c coefficients }; } // Telemetry Bit Sense/Project Name export interface TelemetryBitSensePayload { - type: 'telemetry-bitsense'; - sense: number; // 8-bit sense value + type: "telemetry-bitsense"; + sense: number; // 8-bit sense value projectName?: string; } // Weather Report Payload export interface WeatherPayload { - type: 'weather'; - timestamp?: ITimestamp; - position?: IPosition; - windDirection?: number; // Degrees - windSpeed?: number; // MPH - windGust?: number; // MPH - temperature?: number; // Fahrenheit - rainLastHour?: number; // Hundredths of inch - rainLast24Hours?: number; // Hundredths of inch - rainSinceMidnight?: number; // Hundredths of inch - humidity?: number; // Percent - pressure?: number; // Tenths of millibar - luminosity?: number; // Watts per square meter - snowfall?: number; // Inches - rawRain?: number; // Raw rain counter - software?: string; // Weather software type - weatherUnit?: string; // Weather station type + type: "weather"; + timestamp?: ITimestamp; + position?: IPosition; + windDirection?: number; // Degrees + windSpeed?: number; // MPH + windGust?: number; // MPH + temperature?: number; // Fahrenheit + rainLastHour?: number; // Hundredths of inch + rainLast24Hours?: number; // Hundredths of inch + rainSinceMidnight?: number; // Hundredths of inch + humidity?: number; // Percent + pressure?: number; // Tenths of millibar + luminosity?: number; // Watts per square meter + snowfall?: number; // Inches + rawRain?: number; // Raw rain counter + software?: string; // Weather software type + weatherUnit?: string; // Weather station type } // Raw GPS Payload (NMEA sentences) export interface RawGPSPayload { - type: 'raw-gps'; - sentence: string; // Raw NMEA sentence + type: "raw-gps"; + sentence: string; // Raw NMEA sentence } // Station Capabilities Payload export interface StationCapabilitiesPayload { - type: 'capabilities'; + type: "capabilities"; capabilities: string[]; } // User-Defined Payload export interface UserDefinedPayload { - type: 'user-defined'; + type: "user-defined"; userPacketType: string; data: string; } // Third-Party Traffic Payload export interface ThirdPartyPayload { - type: 'third-party'; - header: string; // Source path of third-party packet - payload: string; // Nested APRS packet + type: "third-party"; + header: string; // Source path of third-party packet + payload: string; // Nested APRS packet } // DF Report Payload export interface DFReportPayload { - type: 'df-report'; + type: "df-report"; timestamp?: ITimestamp; position: IPosition; course?: number; - bearing?: number; // Direction finding bearing - quality?: number; // Signal quality - strength?: number; // Signal strength - height?: number; // Antenna height - gain?: number; // Antenna gain + bearing?: number; // Direction finding bearing + quality?: number; // Signal quality + strength?: number; // Signal strength + height?: number; // Antenna height + gain?: number; // Antenna gain directivity?: string; // Antenna directivity pattern } @@ -296,30 +297,31 @@ export interface BasePayload { } // Union type for all decoded payload types -export type Payload = BasePayload & ( - | PositionPayload - | MicEPayload - | MessagePayload - | BulletinPayload - | ObjectPayload - | ItemPayload - | StatusPayload - | QueryPayload - | TelemetryDataPayload - | TelemetryParameterPayload - | TelemetryUnitPayload - | TelemetryCoefficientsPayload - | TelemetryBitSensePayload - | WeatherPayload - | RawGPSPayload - | StationCapabilitiesPayload - | UserDefinedPayload - | ThirdPartyPayload - | DFReportPayload -); +export type Payload = BasePayload & + ( + | PositionPayload + | MicEPayload + | MessagePayload + | BulletinPayload + | ObjectPayload + | ItemPayload + | StatusPayload + | QueryPayload + | TelemetryDataPayload + | TelemetryParameterPayload + | TelemetryUnitPayload + | TelemetryCoefficientsPayload + | TelemetryBitSensePayload + | WeatherPayload + | RawGPSPayload + | StationCapabilitiesPayload + | UserDefinedPayload + | ThirdPartyPayload + | DFReportPayload + ); // Extended Frame with decoded payload export interface DecodedFrame extends IFrame { - decoded?: Payload; + decoded?: Payload; structure?: PacketStructure; // Routing and other frame-level sections } diff --git a/src/index.ts b/src/index.ts index 5217f4c..9407534 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,14 +1,6 @@ -export { - Frame, - Address, - Timestamp, -} from "./frame"; +export { Frame, Address, Timestamp } from "./frame"; -export { - type IAddress, - type IFrame, - DataTypeIdentifier, -} from "./frame.types"; +export { type IAddress, type IFrame, DataTypeIdentifier } from "./frame.types"; export { type ISymbol, @@ -48,10 +40,3 @@ export { celsiusToFahrenheit, fahrenheitToCelsius, } from "./parser"; -export { - type PacketStructure, - type PacketSegment, - type PacketField, - type PacketFieldBit, - FieldType, -} from "./parser.types"; diff --git a/src/parser.ts b/src/parser.ts index 2b0c9f5..4cfa52c 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -15,14 +15,16 @@ export const base91ToNumber = (str: string): number => { const digit = charCode - 33; // Base91 uses chars 33-123 (! to {) if (digit < 0 || digit >= base) { - throw new Error(`Invalid Base91 character: '${str[i]}' (code ${charCode})`); + throw new Error( + `Invalid Base91 character: '${str[i]}' (code ${charCode})`, + ); } value = value * base + digit; } return value; -} +}; /* Conversions from Freedom Units to whatever the rest of the world uses and understands. */ @@ -38,7 +40,7 @@ const FAHRENHEIT_TO_CELSIUS_OFFSET = 32; */ export const knotsToKmh = (knots: number): number => { return knots * KNOTS_TO_KMH; -} +}; /** * Convert speed from kilometers per hour to knots. @@ -48,7 +50,7 @@ export const knotsToKmh = (knots: number): number => { */ export const kmhToKnots = (kmh: number): number => { return kmh / KNOTS_TO_KMH; -} +}; /** * Convert altitude from feet to meters. @@ -58,7 +60,7 @@ export const kmhToKnots = (kmh: number): number => { */ export const feetToMeters = (feet: number): number => { return feet * FEET_TO_METERS; -} +}; /** * Convert altitude from meters to feet. @@ -68,7 +70,7 @@ export const feetToMeters = (feet: number): number => { */ export const metersToFeet = (meters: number): number => { return meters / FEET_TO_METERS; -} +}; /** * Convert temperature from Celsius to Fahrenheit. @@ -77,8 +79,8 @@ export const metersToFeet = (meters: number): number => { * @returns equivalent temperature in Fahrenheit */ export const celsiusToFahrenheit = (celsius: number): number => { - return (celsius * 9/5) + FAHRENHEIT_TO_CELSIUS_OFFSET; -} + return (celsius * 9) / 5 + FAHRENHEIT_TO_CELSIUS_OFFSET; +}; /** * Convert temperature from Fahrenheit to Celsius. @@ -87,5 +89,5 @@ export const celsiusToFahrenheit = (celsius: number): number => { * @returns equivalent temperature in Celsius */ export const fahrenheitToCelsius = (fahrenheit: number): number => { - return (fahrenheit - FAHRENHEIT_TO_CELSIUS_OFFSET) * 5/9; -} + return ((fahrenheit - FAHRENHEIT_TO_CELSIUS_OFFSET) * 5) / 9; +}; diff --git a/src/parser.types.ts b/src/parser.types.ts deleted file mode 100644 index 569419f..0000000 --- a/src/parser.types.ts +++ /dev/null @@ -1,37 +0,0 @@ -export enum FieldType { - BITS = 0, - UINT8 = 1, - UINT16_LE = 2, - UINT16_BE = 3, - UINT32_LE = 4, - UINT32_BE = 5, - BYTES = 6, // 8-bits per value - WORDS = 7, // 16-bits per value - DWORDS = 8, // 32-bits per value - QWORDS = 9, // 64-bits per value - STRING = 10, - C_STRING = 11, // Null-terminated string - CHAR = 12, // Single ASCII character -} - -// Interface for the parsed packet segments, used for debugging and testing. -export type PacketStructure = PacketSegment[]; - -export interface PacketSegment { - name: string; - data: Uint8Array; - fields: PacketField[]; -} - -export interface PacketField { - type: FieldType; - size: number; // Size in bytes - name?: string; - bits?: PacketFieldBit[]; // Only for bit fields in FieldType.BITS - value?: any; // Optional decoded value -} - -export interface PacketFieldBit { - name: string; - size: number; // Size in bits -} diff --git a/src/position.ts b/src/position.ts index 14f25fb..069832a 100644 --- a/src/position.ts +++ b/src/position.ts @@ -1,8 +1,8 @@ import { IPosition, ISymbol } from "./frame.types"; export class Symbol implements ISymbol { - table: string; // Symbol table identifier - code: string; // Symbol code + table: string; // Symbol table identifier + code: string; // Symbol code constructor(table: string, code?: string) { if (code === undefined) { @@ -10,7 +10,9 @@ export class Symbol implements ISymbol { this.code = table[1]; this.table = table[0]; } else { - throw new Error(`Invalid symbol format: '${table}' (expected 2 characters if code is not provided)`); + throw new Error( + `Invalid symbol format: '${table}' (expected 2 characters if code is not provided)`, + ); } } else { this.table = table; @@ -24,12 +26,12 @@ export class Symbol implements ISymbol { } export class Position implements IPosition { - latitude: number; // Decimal degrees - longitude: number; // Decimal degrees + latitude: number; // Decimal degrees + longitude: number; // Decimal degrees ambiguity?: number; // Position ambiguity (0-4) - altitude?: number; // Meters - speed?: number; // Speed in knots/kmh depending on source - course?: number; // Course in degrees + altitude?: number; // Meters + speed?: number; // Speed in knots/kmh depending on source + course?: number; // Course in degrees symbol?: Symbol; comment?: string; @@ -40,7 +42,7 @@ export class Position implements IPosition { this.altitude = data.altitude; this.speed = data.speed; this.course = data.course; - if (typeof data.symbol === 'string') { + if (typeof data.symbol === "string") { this.symbol = new Symbol(data.symbol); } else if (data.symbol) { this.symbol = new Symbol(data.symbol.table, data.symbol.code); @@ -51,21 +53,21 @@ export class Position implements IPosition { public toString(): string { const latStr = this.latitude.toFixed(5); const lonStr = this.longitude.toFixed(5); - const altStr = this.altitude !== undefined ? `,${this.altitude}m` : ''; + const altStr = this.altitude !== undefined ? `,${this.altitude}m` : ""; return `${latStr},${lonStr}${altStr}`; } public distanceTo(other: IPosition): number { const R = 6371e3; // Earth radius in meters - const lat1 = this.latitude * Math.PI / 180; - const lat2 = other.latitude * Math.PI / 180; - const dLat = (other.latitude - this.latitude) * Math.PI / 180; - const dLon = (other.longitude - this.longitude) * Math.PI / 180; + const lat1 = (this.latitude * Math.PI) / 180; + const lat2 = (other.latitude * Math.PI) / 180; + const dLat = ((other.latitude - this.latitude) * Math.PI) / 180; + const dLon = ((other.longitude - this.longitude) * Math.PI) / 180; - const a = Math.sin(dLat/2) * Math.sin(dLat/2) + - Math.cos(lat1) * Math.cos(lat2) * - Math.sin(dLon/2) * Math.sin(dLon/2); - const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); + const a = + Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) * Math.sin(dLon / 2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; // Distance in meters } diff --git a/test/frame.test.ts b/test/frame.test.ts index 49d9c74..ddfc480 100644 --- a/test/frame.test.ts +++ b/test/frame.test.ts @@ -1,94 +1,103 @@ -import { describe, expect, it } from 'vitest'; -import { Address, Frame, Timestamp } from '../src/frame'; -import type { Payload, PositionPayload, ObjectPayload, StatusPayload, ITimestamp } from '../src/frame.types'; -import { FieldType, PacketSegment, PacketStructure } from '../src/parser.types'; +import { describe, expect, it } from "vitest"; +import { Address, Frame, Timestamp } from "../src/frame"; +import type { + Payload, + PositionPayload, + ObjectPayload, + StatusPayload, + ITimestamp, + MessagePayload, +} from "../src/frame.types"; +import { Dissected, FieldType } from "@hamradio/packet"; // Address parsing: split by method -describe('Address.parse', () => { - it('should parse callsign without SSID', () => { - const result = Address.parse('NOCALL'); - expect(result).toEqual({ call: 'NOCALL', ssid: '', isRepeated: false }); +describe("Address.parse", () => { + it("should parse callsign without SSID", () => { + const result = Address.parse("NOCALL"); + expect(result).toEqual({ call: "NOCALL", ssid: "", isRepeated: false }); }); }); -describe('Address.fromString', () => { - it('should parse callsign with SSID', () => { - const result = Address.fromString('NOCALL-1'); - expect(result).toEqual({ call: 'NOCALL', ssid: '1', isRepeated: false }); +describe("Address.fromString", () => { + it("should parse callsign with SSID", () => { + const result = Address.fromString("NOCALL-1"); + expect(result).toEqual({ call: "NOCALL", ssid: "1", isRepeated: false }); }); - it('should parse repeated address', () => { - const result = Address.fromString('WA1PLE-4*'); - expect(result).toEqual({ call: 'WA1PLE', ssid: '4', isRepeated: true }); + it("should parse repeated address", () => { + const result = Address.fromString("WA1PLE-4*"); + expect(result).toEqual({ call: "WA1PLE", ssid: "4", isRepeated: true }); }); - it('should parse address without SSID but with repeat marker', () => { - const result = Address.fromString('WIDE1*'); - expect(result).toEqual({ call: 'WIDE1', ssid: '', isRepeated: true }); + it("should parse address without SSID but with repeat marker", () => { + const result = Address.fromString("WIDE1*"); + expect(result).toEqual({ call: "WIDE1", ssid: "", isRepeated: true }); }); }); // Frame constructor first -describe('Frame.constructor', () => { - it('returns a Frame instance from Frame.fromString', () => { - const data = 'W1AW>APRS:>Status message'; +describe("Frame.constructor", () => { + it("returns a Frame instance from Frame.fromString", () => { + const data = "W1AW>APRS:>Status message"; const result = Frame.fromString(data); expect(result).toBeInstanceOf(Object); }); }); // Frame properties / instance methods -describe('Frame.getDataTypeIdentifier', () => { - it('returns @ for position identifier', () => { - const data = 'NOCALL-1>APRS,WIDE1-1:@092345z/:*E";qZ=OMRC/A=088132Hello World!'; +describe("Frame.getDataTypeIdentifier", () => { + it("returns @ for position identifier", () => { + const data = + 'NOCALL-1>APRS,WIDE1-1:@092345z/:*E";qZ=OMRC/A=088132Hello World!'; const frame = Frame.fromString(data); - expect(frame.getDataTypeIdentifier()).toBe('@'); + expect(frame.getDataTypeIdentifier()).toBe("@"); }); - it('returns ` for Mic-E identifier', () => { - const data = 'N83MZ>T2TQ5U,WA1PLE-4*:`c.l+@&\'/\'"G:} KJ6TMS|!:&0\'p|!w#f!|3'; + it("returns ` for Mic-E identifier", () => { + const data = "N83MZ>T2TQ5U,WA1PLE-4*:`c.l+@&'/'\"G:} KJ6TMS|!:&0'p|!w#f!|3"; const frame = Frame.fromString(data); - expect(frame.getDataTypeIdentifier()).toBe('`'); + expect(frame.getDataTypeIdentifier()).toBe("`"); }); - it('returns : for message identifier', () => { - const data = 'W1AW>APRS::KB1ABC-5 :Hello World'; + it("returns : for message identifier", () => { + const data = "W1AW>APRS::KB1ABC-5 :Hello World"; const frame = Frame.fromString(data); - expect(frame.getDataTypeIdentifier()).toBe(':'); + expect(frame.getDataTypeIdentifier()).toBe(":"); }); - it('returns > for status identifier', () => { - const data = 'W1AW>APRS:>Status message'; + it("returns > for status identifier", () => { + const data = "W1AW>APRS:>Status message"; const frame = Frame.fromString(data); - expect(frame.getDataTypeIdentifier()).toBe('>'); + expect(frame.getDataTypeIdentifier()).toBe(">"); }); }); -describe('Frame.decode (basic)', () => { - it('should call decode and return position payload', () => { - const data = 'NOCALL-1>APRS,WIDE1-1:@092345z/:*E";qZ=OMRC/A=088132Hello World!'; +describe("Frame.decode (basic)", () => { + it("should call decode and return position payload", () => { + const data = + 'NOCALL-1>APRS,WIDE1-1:@092345z/:*E";qZ=OMRC/A=088132Hello World!'; const frame = Frame.fromString(data); const decoded = frame.decode() as PositionPayload; expect(decoded).not.toBeNull(); - expect(decoded?.type).toBe('position'); + expect(decoded?.type).toBe("position"); }); - it('should handle various data type identifiers without throwing', () => { + it("should handle various data type identifiers without throwing", () => { const testCases = [ - { data: 'CALL>APRS:!4903.50N/07201.75W-', type: '!' }, - { data: 'CALL>APRS:=4903.50N/07201.75W-', type: '=' }, - { data: 'CALL>APRS:/092345z4903.50N/07201.75W>', type: '/' }, - { data: 'CALL>APRS:>Status Text', type: '>' }, - { data: 'CALL>APRS::ADDRESS :Message', type: ':' }, - { data: 'CALL>APRS:;OBJECT *092345z4903.50N/07201.75W>', type: ';' }, - { data: 'CALL>APRS:)ITEM!4903.50N/07201.75W-', type: ')' }, - { data: 'CALL>APRS:?APRS?', type: '?' }, - { data: 'CALL>APRS:T#001,123,456,789', type: 'T' }, - { data: 'CALL>APRS:_10090556c...', type: '_' }, - { data: 'CALL>APRS:$GPRMC,...', type: '$' }, - { data: 'CALL>APRS:APRS:{01', type: '{' }, - { data: 'CALL>APRS:}W1AW>APRS:test', type: '}' }, + { data: "CALL>APRS:!4903.50N/07201.75W-", type: "!" }, + { data: "CALL>APRS:=4903.50N/07201.75W-", type: "=" }, + { data: "CALL>APRS:/092345z4903.50N/07201.75W>", type: "/" }, + { data: "CALL>APRS:>Status Text", type: ">" }, + { data: "CALL>APRS::ADDRESS :Message", type: ":" }, + { data: "CALL>APRS:;OBJECT *092345z4903.50N/07201.75W>", type: ";" }, + { data: "CALL>APRS:)ITEM!4903.50N/07201.75W-", type: ")" }, + { data: "CALL>APRS:?APRS?", type: "?" }, + { data: "CALL>APRS:T#001,123,456,789", type: "T" }, + { data: "CALL>APRS:_10090556c...", type: "_" }, + { data: "CALL>APRS:$GPRMC,...", type: "$" }, + { data: "CALL>APRS:APRS:{01", type: "{" }, + { data: "CALL>APRS:}W1AW>APRS:test", type: "}" }, ]; for (const testCase of testCases) { const frame = Frame.fromString(testCase.data); @@ -99,57 +108,87 @@ describe('Frame.decode (basic)', () => { }); // Static functions -describe('Frame.fromString', () => { - it('parses APRS position frame (test vector 1)', () => { - const data = 'NOCALL-1>APRS,WIDE1-1:@092345z/:*E";qZ=OMRC/A=088132Hello World!'; +describe("Frame.fromString", () => { + it("parses APRS position frame (test vector 1)", () => { + const data = + 'NOCALL-1>APRS,WIDE1-1:@092345z/:*E";qZ=OMRC/A=088132Hello World!'; const result = Frame.fromString(data); - expect(result.source).toEqual({ call: 'NOCALL', ssid: '1', isRepeated: false }); - expect(result.destination).toEqual({ call: 'APRS', ssid: '', isRepeated: false }); + expect(result.source).toEqual({ + call: "NOCALL", + ssid: "1", + isRepeated: false, + }); + expect(result.destination).toEqual({ + call: "APRS", + ssid: "", + isRepeated: false, + }); expect(result.path).toHaveLength(1); - expect(result.path[0]).toEqual({ call: 'WIDE1', ssid: '1', isRepeated: false }); + expect(result.path[0]).toEqual({ + call: "WIDE1", + ssid: "1", + isRepeated: false, + }); expect(result.payload).toBe('@092345z/:*E";qZ=OMRC/A=088132Hello World!'); }); - it('parses APRS Mic-E frame with repeated digipeater (test vector 2)', () => { - const data = 'N83MZ>T2TQ5U,WA1PLE-4*:`c.l+@&\'/\'"G:} KJ6TMS|!:&0\'p|!w#f!|3'; + it("parses APRS Mic-E frame with repeated digipeater (test vector 2)", () => { + const data = "N83MZ>T2TQ5U,WA1PLE-4*:`c.l+@&'/'\"G:} KJ6TMS|!:&0'p|!w#f!|3"; const result = Frame.fromString(data); - expect(result.source).toEqual({ call: 'N83MZ', ssid: '', isRepeated: false }); - expect(result.destination).toEqual({ call: 'T2TQ5U', ssid: '', isRepeated: false }); + expect(result.source).toEqual({ + call: "N83MZ", + ssid: "", + isRepeated: false, + }); + expect(result.destination).toEqual({ + call: "T2TQ5U", + ssid: "", + isRepeated: false, + }); expect(result.path).toHaveLength(1); - expect(result.path[0]).toEqual({ call: 'WA1PLE', ssid: '4', isRepeated: true }); - expect(result.payload).toBe('`c.l+@&\'/\'"G:} KJ6TMS|!:&0\'p|!w#f!|3'); + expect(result.path[0]).toEqual({ + call: "WA1PLE", + ssid: "4", + isRepeated: true, + }); + expect(result.payload).toBe("`c.l+@&'/'\"G:} KJ6TMS|!:&0'p|!w#f!|3"); }); - it('parses frame with multiple path elements', () => { - const data = 'KB1ABC-5>APRS,WIDE1-1,WIDE2-2*,IGATE:!4903.50N/07201.75W-Test'; + it("parses frame with multiple path elements", () => { + const data = + "KB1ABC-5>APRS,WIDE1-1,WIDE2-2*,IGATE:!4903.50N/07201.75W-Test"; const result = Frame.fromString(data); - expect(result.source.call).toBe('KB1ABC'); + expect(result.source.call).toBe("KB1ABC"); expect(result.path).toHaveLength(3); - expect(result.payload).toBe('!4903.50N/07201.75W-Test'); + expect(result.payload).toBe("!4903.50N/07201.75W-Test"); }); - it('parses frame with no path', () => { - const data = 'W1AW>APRS::STATUS:Testing'; + it("parses frame with no path", () => { + const data = "W1AW>APRS::STATUS:Testing"; const result = Frame.fromString(data); expect(result.path).toHaveLength(0); - expect(result.payload).toBe(':STATUS:Testing'); + expect(result.payload).toBe(":STATUS:Testing"); }); - it('throws for frame without route separator', () => { - const data = 'NOCALL-1>APRS'; - expect(() => Frame.fromString(data)).toThrow('APRS: invalid frame, no route separator found'); + it("throws for frame without route separator", () => { + const data = "NOCALL-1>APRS"; + expect(() => Frame.fromString(data)).toThrow( + "APRS: invalid frame, no route separator found", + ); }); - it('throws for frame with invalid addresses', () => { - const data = 'NOCALL:payload'; - expect(() => Frame.fromString(data)).toThrow('APRS: invalid addresses in route'); + it("throws for frame with invalid addresses", () => { + const data = "NOCALL:payload"; + expect(() => Frame.fromString(data)).toThrow( + "APRS: invalid addresses in route", + ); }); }); // Timestamp class (toDate / constructors) -describe('Timestamp.toDate', () => { - it('creates DHM timestamp and converts to Date', () => { - const ts = new Timestamp(14, 30, 'DHM', { day: 15, zulu: true }); +describe("Timestamp.toDate", () => { + it("creates DHM timestamp and converts to Date", () => { + const ts = new Timestamp(14, 30, "DHM", { day: 15, zulu: true }); expect(ts.hours).toBe(14); expect(ts.minutes).toBe(30); expect(ts.day).toBe(15); @@ -158,15 +197,15 @@ describe('Timestamp.toDate', () => { expect(date.getUTCDate()).toBe(15); }); - it('creates HMS timestamp and converts to Date', () => { - const ts = new Timestamp(12, 45, 'HMS', { seconds: 30, zulu: true }); + it("creates HMS timestamp and converts to Date", () => { + const ts = new Timestamp(12, 45, "HMS", { seconds: 30, zulu: true }); const date = ts.toDate(); expect(date).toBeInstanceOf(Date); expect(date.getUTCHours()).toBe(12); }); - it('creates MDHM timestamp and converts to Date', () => { - const ts = new Timestamp(16, 20, 'MDHM', { month: 3, day: 5, zulu: false }); + it("creates MDHM timestamp and converts to Date", () => { + const ts = new Timestamp(16, 20, "MDHM", { month: 3, day: 5, zulu: false }); const date = ts.toDate(); expect(date).toBeInstanceOf(Date); expect(date.getMonth()).toBe(2); @@ -174,135 +213,154 @@ describe('Timestamp.toDate', () => { }); // Private decode functions (alphabetical order) -describe('Frame.decodeMicE', () => { - it('decodes a basic Mic-E packet (current format)', () => { - const data = 'N83MZ>T2TQ5U,WA1PLE-4*:`c.l+@&\'/\'"G:} KJ6TMS|!:&0\'p|!w#f!|3'; +describe("Frame.decodeMicE", () => { + it("decodes a basic Mic-E packet (current format)", () => { + const data = "N83MZ>T2TQ5U,WA1PLE-4*:`c.l+@&'/'\"G:} KJ6TMS|!:&0'p|!w#f!|3"; const frame = Frame.fromString(data); const decoded = frame.decode() as Payload; expect(decoded).not.toBeNull(); - expect(decoded?.type).toBe('position'); + expect(decoded?.type).toBe("position"); }); - it('decodes a Mic-E packet with old format (single quote)', () => { - const data = 'CALL>T2TQ5U:\'c.l+@&\'/\'"G:}'; + it("decodes a Mic-E packet with old format (single quote)", () => { + const data = "CALL>T2TQ5U:'c.l+@&'/'\"G:}"; const frame = Frame.fromString(data); const decoded = frame.decode() as Payload; expect(decoded).not.toBeNull(); - expect(decoded?.type).toBe('position'); + expect(decoded?.type).toBe("position"); }); }); -describe('Frame.decodeMessage', () => { - it('decodes a standard APRS message with 9-char recipient field', () => { - const raw = 'W1AW>APRS::KB1ABC-5 :Hello World'; +describe("Frame.decodeMessage", () => { + it("decodes a standard APRS message with 9-char recipient field", () => { + const raw = "W1AW>APRS::KB1ABC-5 :Hello World"; const frame = Frame.fromString(raw); - const decoded = frame.decode() as any; + const decoded = frame.decode() as MessagePayload; expect(decoded).not.toBeNull(); - expect(decoded?.type).toBe('message'); - expect(decoded?.to).toBe('KB1ABC-5'); - expect(decoded?.text).toBe('Hello World'); + expect(decoded?.type).toBe("message"); + expect(decoded?.addressee).toBe("KB1ABC-5"); + expect(decoded?.text).toBe("Hello World"); }); - it('emits recipient and text sections when emitSections is true', () => { - const raw = 'W1AW>APRS::KB1ABC :Test via spec'; + it("emits recipient and text sections when emitSections is true", () => { + const raw = "W1AW>APRS::KB1ABC :Test via spec"; const frame = Frame.fromString(raw); - const res = frame.decode(true) as { payload: any; structure: PacketStructure }; + const res = frame.decode(true) as { + payload: MessagePayload | null; + structure: Dissected; + }; expect(res.payload).not.toBeNull(); - expect(res.payload.type).toBe('message'); - const recipientSection = res.structure.find(s => s.name === 'recipient'); - const textSection = res.structure.find(s => s.name === 'text'); + expect(res.payload?.type).toBe("message"); + const recipientSection = res.structure.find((s) => s.name === "recipient"); + const textSection = res.structure.find((s) => s.name === "text"); expect(recipientSection).toBeDefined(); expect(textSection).toBeDefined(); - expect(new TextDecoder().decode(recipientSection!.data).trim()).toBe('KB1ABC'); - expect(new TextDecoder().decode(textSection!.data)).toBe('Test via spec'); + expect(new TextDecoder().decode(recipientSection!.data).trim()).toBe( + "KB1ABC", + ); + expect(new TextDecoder().decode(textSection!.data)).toBe("Test via spec"); }); }); -describe('Frame.decodeObject', () => { - it('decodes object payload with uncompressed position', () => { - const data = 'CALL>APRS:;OBJECT *092345z4903.50N/07201.75W>Test object'; +describe("Frame.decodeObject", () => { + it("decodes object payload with uncompressed position", () => { + const data = "CALL>APRS:;OBJECT *092345z4903.50N/07201.75W>Test object"; const frame = Frame.fromString(data); const decoded = frame.decode() as Payload; expect(decoded).not.toBeNull(); - expect(decoded?.type).toBe('object'); - if (decoded && decoded.type === 'object') { - expect(decoded.name).toBe('OBJECT'); + expect(decoded?.type).toBe("object"); + if (decoded && decoded.type === "object") { + expect(decoded.name).toBe("OBJECT"); expect(decoded.alive).toBe(true); } }); - it('emits object sections when emitSections is true', () => { - const data = 'CALL>APRS:;OBJECT *092345z4903.50N/07201.75W>Test object'; + it("emits object sections when emitSections is true", () => { + const data = "CALL>APRS:;OBJECT *092345z4903.50N/07201.75W>Test object"; const frame = Frame.fromString(data); - const result = frame.decode(true) as { payload: Payload | null; structure: PacketStructure }; + const result = frame.decode(true) as { + payload: ObjectPayload | null; + structure: Dissected; + }; expect(result.payload).not.toBeNull(); expect(result.structure.length).toBeGreaterThan(0); }); }); -describe('Frame.decodePosition', () => { - it('decodes position with timestamp and compressed format', () => { - const data = 'NOCALL-1>APRS,WIDE1-1:@092345z/:*E";qZ=OMRC/A=088132Hello World!'; +describe("Frame.decodePosition", () => { + it("decodes position with timestamp and compressed format", () => { + const data = + 'NOCALL-1>APRS,WIDE1-1:@092345z/:*E";qZ=OMRC/A=088132Hello World!'; const frame = Frame.fromString(data); const decoded = frame.decode() as PositionPayload; expect(decoded).not.toBeNull(); - expect(decoded?.type).toBe('position'); + expect(decoded?.type).toBe("position"); }); - it('decodes uncompressed position without timestamp', () => { - const data = 'KB1ABC>APRS:!4903.50N/07201.75W-Test message'; + it("decodes uncompressed position without timestamp", () => { + const data = "KB1ABC>APRS:!4903.50N/07201.75W-Test message"; const frame = Frame.fromString(data); - const decoded = frame.decode() as Payload; + const decoded = frame.decode() as PositionPayload; expect(decoded).not.toBeNull(); - expect(decoded?.type).toBe('position'); + expect(decoded?.type).toBe("position"); }); - it('handles ambiguity masking in position', () => { - const data = 'CALL>APRS:!4903.5 N/07201.75W-'; + it("handles ambiguity masking in position", () => { + const data = "CALL>APRS:!4903.5 N/07201.75W-"; const frame = Frame.fromString(data); - const decoded = frame.decode() as Payload; + const decoded = frame.decode() as PositionPayload; expect(decoded).not.toBeNull(); }); }); -describe('Frame.decodeStatus', () => { - it('decodes a status payload with timestamp and Maidenhead', () => { - const raw = 'N0CALL>APRS:>120912zTesting status FN20'; +describe("Frame.decodeStatus", () => { + it("decodes a status payload with timestamp and Maidenhead", () => { + const raw = "N0CALL>APRS:>120912zTesting status FN20"; const frame = Frame.parse(raw); - const res = frame.decode(true) as { payload: Payload | null; structure: PacketStructure }; + const res = frame.decode(true) as { + payload: StatusPayload | null; + structure: Dissected; + }; expect(res.payload).not.toBeNull(); - if (res.payload?.type !== 'status') throw new Error('expected status payload'); + if (res.payload?.type !== "status") + throw new Error("expected status payload"); const payload = res.payload as StatusPayload & { timestamp?: ITimestamp }; - expect(payload.text).toBe('Testing status'); - expect(payload.maidenhead).toBe('FN20'); + expect(payload.text).toBe("Testing status"); + expect(payload.maidenhead).toBe("FN20"); }); }); // Packet dissection and sections -describe('Frame.decode (sections)', () => { - it('emits routing sections when emitSections is true', () => { - const data = 'KB1ABC-5>APRS,WIDE1-1,WIDE2-2*:!4903.50N/07201.75W-Test'; +describe("Frame.decode (sections)", () => { + it("emits routing sections when emitSections is true", () => { + const data = "KB1ABC-5>APRS,WIDE1-1,WIDE2-2*:!4903.50N/07201.75W-Test"; const frame = Frame.fromString(data); - const result = frame.decode(true) as { payload: Payload; structure: PacketStructure }; + const result = frame.decode(true) as { + payload: ObjectPayload | null; + structure: Dissected; + }; expect(result.structure.length).toBeGreaterThan(0); }); - it('emits position payload sections when emitSections is true', () => { - const data = 'CALL>APRS:!4903.50N/07201.75W-Test message'; + it("emits position payload sections when emitSections is true", () => { + const data = "CALL>APRS:!4903.50N/07201.75W-Test message"; const frame = Frame.fromString(data); - const result = frame.decode(true) as { payload: Payload; structure: PacketStructure }; - expect(result.payload?.type).toBe('position'); + const result = frame.decode(true) as { + payload: PositionPayload | null; + structure: Dissected; + }; + expect(result.payload?.type).toBe("position"); }); }); -describe('Timestamp.toDate', () => { - it('should create DHM timestamp and convert to Date', () => { - const ts = new Timestamp(14, 30, 'DHM', { day: 15, zulu: true }); +describe("Timestamp.toDate", () => { + it("should create DHM timestamp and convert to Date", () => { + const ts = new Timestamp(14, 30, "DHM", { day: 15, zulu: true }); expect(ts.hours).toBe(14); expect(ts.minutes).toBe(30); expect(ts.day).toBe(15); - expect(ts.format).toBe('DHM'); + expect(ts.format).toBe("DHM"); expect(ts.zulu).toBe(true); const date = ts.toDate(); @@ -312,13 +370,13 @@ describe('Timestamp.toDate', () => { expect(date.getUTCMinutes()).toBe(30); }); - it('should create HMS timestamp and convert to Date', () => { - const ts = new Timestamp(12, 45, 'HMS', { seconds: 30, zulu: true }); + it("should create HMS timestamp and convert to Date", () => { + const ts = new Timestamp(12, 45, "HMS", { seconds: 30, zulu: true }); expect(ts.hours).toBe(12); expect(ts.minutes).toBe(45); expect(ts.seconds).toBe(30); - expect(ts.format).toBe('HMS'); + expect(ts.format).toBe("HMS"); const date = ts.toDate(); expect(date).toBeInstanceOf(Date); @@ -327,14 +385,14 @@ describe('Timestamp.toDate', () => { expect(date.getUTCSeconds()).toBe(30); }); - it('should create MDHM timestamp and convert to Date', () => { - const ts = new Timestamp(16, 20, 'MDHM', { month: 3, day: 5, zulu: false }); + it("should create MDHM timestamp and convert to Date", () => { + const ts = new Timestamp(16, 20, "MDHM", { month: 3, day: 5, zulu: false }); expect(ts.hours).toBe(16); expect(ts.minutes).toBe(20); expect(ts.month).toBe(3); expect(ts.day).toBe(5); - expect(ts.format).toBe('MDHM'); + expect(ts.format).toBe("MDHM"); expect(ts.zulu).toBe(false); const date = ts.toDate(); @@ -345,23 +403,26 @@ describe('Timestamp.toDate', () => { expect(date.getMinutes()).toBe(20); }); - it('should handle DHM timestamp that is in the future (use previous month)', () => { + it("should handle DHM timestamp that is in the future (use previous month)", () => { const now = new Date(); const futureDay = now.getUTCDate() + 5; - const ts = new Timestamp(12, 0, 'DHM', { day: futureDay, zulu: true }); + const ts = new Timestamp(12, 0, "DHM", { day: futureDay, zulu: true }); const date = ts.toDate(); // Should be in the past or very close to now expect(date <= now).toBe(true); }); - it('should handle HMS timestamp that is in the future (use yesterday)', () => { + it("should handle HMS timestamp that is in the future (use yesterday)", () => { const now = new Date(); const futureHours = now.getUTCHours() + 2; if (futureHours < 24) { - const ts = new Timestamp(futureHours, 0, 'HMS', { seconds: 0, zulu: true }); + const ts = new Timestamp(futureHours, 0, "HMS", { + seconds: 0, + zulu: true, + }); const date = ts.toDate(); // Should be in the past @@ -369,15 +430,15 @@ describe('Timestamp.toDate', () => { } }); - it('should handle MDHM timestamp that is in the future (use last year)', () => { + it("should handle MDHM timestamp that is in the future (use last year)", () => { const now = new Date(); const futureMonth = now.getMonth() + 2; if (futureMonth < 12) { - const ts = new Timestamp(12, 0, 'MDHM', { + const ts = new Timestamp(12, 0, "MDHM", { month: futureMonth + 1, day: 1, - zulu: false + zulu: false, }); const date = ts.toDate(); @@ -389,152 +450,153 @@ describe('Timestamp.toDate', () => { // Private decode functions and related parsing tests (alphabetical grouping) -describe('Frame.decodeMicE', () => { - describe('Basic Mic-E frames', () => { - it('should decode a basic Mic-E packet (current format)', () => { - const data = 'N83MZ>T2TQ5U,WA1PLE-4*:`c.l+@&\'/\'"G:} KJ6TMS|!:&0\'p|!w#f!|3'; +describe("Frame.decodeMicE", () => { + describe("Basic Mic-E frames", () => { + it("should decode a basic Mic-E packet (current format)", () => { + const data = + "N83MZ>T2TQ5U,WA1PLE-4*:`c.l+@&'/'\"G:} KJ6TMS|!:&0'p|!w#f!|3"; const frame = Frame.fromString(data); const decoded = frame.decode() as Payload; expect(decoded).not.toBeNull(); - expect(decoded?.type).toBe('position'); + expect(decoded?.type).toBe("position"); - if (decoded && decoded.type === 'position') { + if (decoded && decoded.type === "position") { expect(decoded.messaging).toBe(true); expect(decoded.position).toBeDefined(); - expect(typeof decoded.position.latitude).toBe('number'); - expect(typeof decoded.position.longitude).toBe('number'); + expect(typeof decoded.position.latitude).toBe("number"); + expect(typeof decoded.position.longitude).toBe("number"); expect(decoded.position.symbol).toBeDefined(); expect(decoded.micE).toBeDefined(); expect(decoded.micE?.messageType).toBeDefined(); } }); - it('should decode a Mic-E packet with old format (single quote)', () => { - const data = 'CALL>T2TQ5U:\'c.l+@&\'/\'"G:}'; + it("should decode a Mic-E packet with old format (single quote)", () => { + const data = "CALL>T2TQ5U:'c.l+@&'/'\"G:}"; const frame = Frame.fromString(data); const decoded = frame.decode() as Payload; expect(decoded).not.toBeNull(); - expect(decoded?.type).toBe('position'); + expect(decoded?.type).toBe("position"); }); }); - describe('Frame.decodeLatitude', () => { - it('should decode latitude from numeric digits (0-9)', () => { - const data = 'CALL>123456:`c.l+@&\'/\'"G:}'; + describe("Frame.decodeLatitude", () => { + it("should decode latitude from numeric digits (0-9)", () => { + const data = "CALL>123456:`c.l+@&'/'\"G:}"; const frame = Frame.fromString(data); const decoded = frame.decode() as Payload; expect(decoded).not.toBeNull(); - if (decoded && decoded.type === 'position') { - expect(decoded.position.latitude).toBeCloseTo(12 + 34.56/60, 3); + if (decoded && decoded.type === "position") { + expect(decoded.position.latitude).toBeCloseTo(12 + 34.56 / 60, 3); } }); - it('should decode latitude from letter digits (A-J)', () => { - const data = 'CALL>ABC0EF:`c.l+@&\'/\'"G:}'; + it("should decode latitude from letter digits (A-J)", () => { + const data = "CALL>ABC0EF:`c.l+@&'/'\"G:}"; const frame = Frame.fromString(data); const decoded = frame.decode() as Payload; expect(decoded).not.toBeNull(); - if (decoded && decoded.type === 'position') { - expect(decoded.position.latitude).toBeCloseTo(1 + 20.45/60, 3); + if (decoded && decoded.type === "position") { + expect(decoded.position.latitude).toBeCloseTo(1 + 20.45 / 60, 3); } }); - it('should decode latitude with mixed digits and letters', () => { - const data = 'CALL>4AB2DE:`c.l+@&\'/\'"G:}'; + it("should decode latitude with mixed digits and letters", () => { + const data = "CALL>4AB2DE:`c.l+@&'/'\"G:}"; const frame = Frame.fromString(data); const decoded = frame.decode() as Payload; expect(decoded).not.toBeNull(); - if (decoded && decoded.type === 'position') { - expect(decoded.position.latitude).toBeCloseTo(40 + 12.34/60, 3); + if (decoded && decoded.type === "position") { + expect(decoded.position.latitude).toBeCloseTo(40 + 12.34 / 60, 3); } }); - it('should decode latitude for southern hemisphere', () => { - const data = 'CALL>4A0P0U:`c.l+@&\'/\'"G:}'; + it("should decode latitude for southern hemisphere", () => { + const data = "CALL>4A0P0U:`c.l+@&'/'\"G:}"; const frame = Frame.fromString(data); const decoded = frame.decode() as Payload; expect(decoded).not.toBeNull(); - if (decoded && decoded.type === 'position') { + if (decoded && decoded.type === "position") { expect(decoded.position.latitude).toBeLessThan(0); } }); }); - describe('Longitude decoding from information field', () => { - it('should decode longitude from information field', () => { - const data = 'CALL>4ABCDE:`c.l+@&\'/\'"G:}'; + describe("Longitude decoding from information field", () => { + it("should decode longitude from information field", () => { + const data = "CALL>4ABCDE:`c.l+@&'/'\"G:}"; const frame = Frame.fromString(data); const decoded = frame.decode() as Payload; expect(decoded).not.toBeNull(); - if (decoded && decoded.type === 'position') { - expect(typeof decoded.position.longitude).toBe('number'); + if (decoded && decoded.type === "position") { + expect(typeof decoded.position.longitude).toBe("number"); expect(decoded.position.longitude).toBeGreaterThanOrEqual(-180); expect(decoded.position.longitude).toBeLessThanOrEqual(180); } }); - it('should handle eastern hemisphere longitude', () => { - const data = 'CALL>4ABPDE:`c.l+@&\'/\'"G:}'; + it("should handle eastern hemisphere longitude", () => { + const data = "CALL>4ABPDE:`c.l+@&'/'\"G:}"; const frame = Frame.fromString(data); const decoded = frame.decode() as Payload; expect(decoded).not.toBeNull(); - if (decoded && decoded.type === 'position') { - expect(typeof decoded.position.longitude).toBe('number'); + if (decoded && decoded.type === "position") { + expect(typeof decoded.position.longitude).toBe("number"); } }); - it('should handle longitude offset +100', () => { - const data = 'CALL>4ABCDP:`c.l+@&\'/\'"G:}'; + it("should handle longitude offset +100", () => { + const data = "CALL>4ABCDP:`c.l+@&'/'\"G:}"; const frame = Frame.fromString(data); const decoded = frame.decode() as Payload; expect(decoded).not.toBeNull(); - if (decoded && decoded.type === 'position') { - expect(typeof decoded.position.longitude).toBe('number'); + if (decoded && decoded.type === "position") { + expect(typeof decoded.position.longitude).toBe("number"); expect(Math.abs(decoded.position.longitude)).toBeGreaterThan(90); } }); }); - describe('Speed and course decoding', () => { - it('should decode speed from information field', () => { - const data = 'CALL>4ABCDE:`c.l+@&\'/\'"G:}Speed test'; + describe("Speed and course decoding", () => { + it("should decode speed from information field", () => { + const data = "CALL>4ABCDE:`c.l+@&'/'\"G:}Speed test"; const frame = Frame.fromString(data); const decoded = frame.decode() as Payload; expect(decoded).not.toBeNull(); - if (decoded && decoded.type === 'position') { + if (decoded && decoded.type === "position") { if (decoded.position.speed !== undefined) { expect(decoded.position.speed).toBeGreaterThanOrEqual(0); - expect(typeof decoded.position.speed).toBe('number'); + expect(typeof decoded.position.speed).toBe("number"); } } }); - it('should decode course from information field', () => { - const data = 'CALL>4ABCDE:`c.l+@&\'/\'"G:}'; + it("should decode course from information field", () => { + const data = "CALL>4ABCDE:`c.l+@&'/'\"G:}"; const frame = Frame.fromString(data); const decoded = frame.decode() as Payload; expect(decoded).not.toBeNull(); - if (decoded && decoded.type === 'position') { + if (decoded && decoded.type === "position") { if (decoded.position.course !== undefined) { expect(decoded.position.course).toBeGreaterThanOrEqual(0); expect(decoded.position.course).toBeLessThan(360); @@ -542,26 +604,26 @@ describe('Frame.decodeMicE', () => { } }); - it('should not include zero speed in result', () => { - const data = 'CALL>4ABCDE:`\x1c\x1c\x1c\x1c\x1c\x1c/>}'; + it("should not include zero speed in result", () => { + const data = "CALL>4ABCDE:`\x1c\x1c\x1c\x1c\x1c\x1c/>}"; const frame = Frame.fromString(data); const decoded = frame.decode() as Payload; expect(decoded).not.toBeNull(); - if (decoded && decoded.type === 'position') { + if (decoded && decoded.type === "position") { expect(decoded.position.speed).toBeUndefined(); } }); - it('should not include zero or 360+ course in result', () => { - const data = 'CALL>4ABCDE:`c.l\x1c\x1c\x1c/>}'; + it("should not include zero or 360+ course in result", () => { + const data = "CALL>4ABCDE:`c.l\x1c\x1c\x1c/>}"; const frame = Frame.fromString(data); const decoded = frame.decode() as Payload; expect(decoded).not.toBeNull(); - if (decoded && decoded.type === 'position') { + if (decoded && decoded.type === "position") { if (decoded.position.course !== undefined) { expect(decoded.position.course).toBeGreaterThan(0); expect(decoded.position.course).toBeLessThan(360); @@ -570,188 +632,189 @@ describe('Frame.decodeMicE', () => { }); }); - describe('Symbol decoding', () => { - it('should decode symbol table and code', () => { - const data = 'CALL>4ABCDE:`c.l+@&\'/\'"G:}'; + describe("Symbol decoding", () => { + it("should decode symbol table and code", () => { + const data = "CALL>4ABCDE:`c.l+@&'/'\"G:}"; const frame = Frame.fromString(data); const decoded = frame.decode() as Payload; expect(decoded).not.toBeNull(); - if (decoded && decoded.type === 'position') { + if (decoded && decoded.type === "position") { expect(decoded.position.symbol).toBeDefined(); expect(decoded.position.symbol?.table).toBeDefined(); expect(decoded.position.symbol?.code).toBeDefined(); - expect(typeof decoded.position.symbol?.table).toBe('string'); - expect(typeof decoded.position.symbol?.code).toBe('string'); + expect(typeof decoded.position.symbol?.table).toBe("string"); + expect(typeof decoded.position.symbol?.code).toBe("string"); } }); }); - describe('Altitude decoding', () => { - it('should decode altitude from /A=NNNNNN format', () => { - const data = 'CALL>4ABCDE:`c.l+@&\'/\'"G:}/A=001234'; + describe("Altitude decoding", () => { + it("should decode altitude from /A=NNNNNN format", () => { + const data = "CALL>4ABCDE:`c.l+@&'/'\"G:}/A=001234"; const frame = Frame.fromString(data); const decoded = frame.decode() as Payload; expect(decoded).not.toBeNull(); - if (decoded && decoded.type === 'position') { + if (decoded && decoded.type === "position") { expect(decoded.position.altitude).toBeCloseTo(1234 * 0.3048, 1); } }); - it('should decode altitude from base-91 format }abc', () => { - const data = 'CALL>4AB2DE:`c.l+@&\'/\'"G:}}S^X'; + it("should decode altitude from base-91 format }abc", () => { + const data = "CALL>4AB2DE:`c.l+@&'/'\"G:}}S^X"; const frame = Frame.fromString(data); const decoded = frame.decode() as Payload; expect(decoded).not.toBeNull(); - if (decoded && decoded.type === 'position') { - if (decoded.position.comment?.startsWith('}')) { + if (decoded && decoded.type === "position") { + if (decoded.position.comment?.startsWith("}")) { expect(decoded.position.altitude).toBeDefined(); } } }); - it('should prefer /A= format over base-91 when both present', () => { - const data = 'CALL>4ABCDE:`c.l+@&\'/\'"G:}}!!!/A=005000'; + it("should prefer /A= format over base-91 when both present", () => { + const data = "CALL>4ABCDE:`c.l+@&'/'\"G:}}!!!/A=005000"; const frame = Frame.fromString(data); const decoded = frame.decode() as Payload; expect(decoded).not.toBeNull(); - if (decoded && decoded.type === 'position') { + if (decoded && decoded.type === "position") { expect(decoded.position.altitude).toBeCloseTo(5000 * 0.3048, 1); } }); - it('should handle comment without altitude', () => { - const data = 'CALL>4ABCDE:`c.l+@&\'/\'"G:}Just a comment'; + it("should handle comment without altitude", () => { + const data = "CALL>4ABCDE:`c.l+@&'/'\"G:}Just a comment"; const frame = Frame.fromString(data); const decoded = frame.decode() as Payload; expect(decoded).not.toBeNull(); - if (decoded && decoded.type === 'position') { + if (decoded && decoded.type === "position") { expect(decoded.position.altitude).toBeUndefined(); - expect(decoded.position.comment).toContain('Just a comment'); + expect(decoded.position.comment).toContain("Just a comment"); } }); }); - describe('Frame.decodeMessage', () => { - it('should decode message type M0 (Off Duty)', () => { - const data = 'CALL>012345:`c.l+@&\'/\'"G:}'; + describe("Frame.decodeMessage", () => { + it("should decode message type M0 (Off Duty)", () => { + const data = "CALL>012345:`c.l+@&'/'\"G:}"; const frame = Frame.fromString(data); const decoded = frame.decode() as Payload; expect(decoded).not.toBeNull(); - if (decoded && decoded.type === 'position') { - expect(decoded.micE?.messageType).toBe('M0: Off Duty'); + if (decoded && decoded.type === "position") { + expect(decoded.micE?.messageType).toBe("M0: Off Duty"); } }); - it('should decode message type M7 (Emergency)', () => { - const data = 'CALL>ABCDEF:`c.l+@&\'/\'"G:}'; + it("should decode message type M7 (Emergency)", () => { + const data = "CALL>ABCDEF:`c.l+@&'/'\"G:}"; const frame = Frame.fromString(data); const decoded = frame.decode() as Payload; expect(decoded).not.toBeNull(); - if (decoded && decoded.type === 'position') { + if (decoded && decoded.type === "position") { expect(decoded.micE?.messageType).toBeDefined(); - expect(typeof decoded.micE?.messageType).toBe('string'); + expect(typeof decoded.micE?.messageType).toBe("string"); } }); - it('should decode standard vs custom message indicator', () => { - const data = 'CALL>ABCDEF:`c.l+@&\'/\'"G:}'; + it("should decode standard vs custom message indicator", () => { + const data = "CALL>ABCDEF:`c.l+@&'/'\"G:}"; const frame = Frame.fromString(data); const decoded = frame.decode() as Payload; expect(decoded).not.toBeNull(); - if (decoded && decoded.type === 'position') { + if (decoded && decoded.type === "position") { expect(decoded.micE?.isStandard).toBeDefined(); - expect(typeof decoded.micE?.isStandard).toBe('boolean'); + expect(typeof decoded.micE?.isStandard).toBe("boolean"); } }); }); - describe('Comment and telemetry', () => { - it('should extract comment from remaining data', () => { - const data = 'CALL>4ABCDE:`c.l+@&\'/\'"G:}This is a test comment'; + describe("Comment and telemetry", () => { + it("should extract comment from remaining data", () => { + const data = "CALL>4ABCDE:`c.l+@&'/'\"G:}This is a test comment"; const frame = Frame.fromString(data); const decoded = frame.decode() as Payload; expect(decoded).not.toBeNull(); - if (decoded && decoded.type === 'position') { - expect(decoded.position.comment).toContain('This is a test comment'); + if (decoded && decoded.type === "position") { + expect(decoded.position.comment).toContain("This is a test comment"); } }); - it('should handle empty comment', () => { - const data = 'CALL>4ABCDE:`c.l+@&\'/\'"G:}'; + it("should handle empty comment", () => { + const data = "CALL>4ABCDE:`c.l+@&'/'\"G:}"; const frame = Frame.fromString(data); const decoded = frame.decode() as Payload; expect(decoded).not.toBeNull(); - if (decoded && decoded.type === 'position') { + if (decoded && decoded.type === "position") { expect(decoded.position.comment).toBeDefined(); } }); }); - describe('Error handling', () => { - it('should return null for destination address too short', () => { - const data = 'CALL>SHORT:`c.l+@&\'/\'"G:}'; + describe("Error handling", () => { + it("should return null for destination address too short", () => { + const data = "CALL>SHORT:`c.l+@&'/'\"G:}"; const frame = Frame.fromString(data); const decoded = frame.decode() as Payload; expect(decoded).toBeNull(); }); - it('should return null for payload too short', () => { - const data = 'CALL>4ABCDE:`short'; + it("should return null for payload too short", () => { + const data = "CALL>4ABCDE:`short"; const frame = Frame.fromString(data); const decoded = frame.decode() as Payload; expect(decoded).toBeNull(); }); - it('should return null for invalid destination characters', () => { - const data = 'CALL>4@BC#E:`c.l+@&\'/\'"G:}'; + it("should return null for invalid destination characters", () => { + const data = "CALL>4@BC#E:`c.l+@&'/'\"G:}"; const frame = Frame.fromString(data); const decoded = frame.decode() as Payload; expect(decoded).toBeNull(); }); - it('should handle exceptions gracefully', () => { - const data = 'CALL>4ABCDE:`\x00\x00\x00\x00\x00\x00\x00\x00'; + it("should handle exceptions gracefully", () => { + const data = "CALL>4ABCDE:`\x00\x00\x00\x00\x00\x00\x00\x00"; const frame = Frame.fromString(data); expect(() => frame.decode()).not.toThrow(); const decoded = frame.decode() as Payload; - expect(decoded === null || decoded?.type === 'position').toBe(true); + expect(decoded === null || decoded?.type === "position").toBe(true); }); }); - describe('Real-world test vectors', () => { - it('should decode real Mic-E packet from test vector 2', () => { - const data = 'N83MZ>T2TQ5U,WA1PLE-4*:`c.l+@&\'/\'"G:} KJ6TMS|!:&0\'p|!w#f!|3'; + describe("Real-world test vectors", () => { + it("should decode real Mic-E packet from test vector 2", () => { + const data = + "N83MZ>T2TQ5U,WA1PLE-4*:`c.l+@&'/'\"G:} KJ6TMS|!:&0'p|!w#f!|3"; const frame = Frame.fromString(data); const decoded = frame.decode() as Payload; expect(decoded).not.toBeNull(); - expect(decoded?.type).toBe('position'); + expect(decoded?.type).toBe("position"); - if (decoded && decoded.type === 'position') { + if (decoded && decoded.type === "position") { expect(decoded.messaging).toBe(true); expect(decoded.position.latitude).toBeDefined(); expect(decoded.position.longitude).toBeDefined(); @@ -764,163 +827,198 @@ describe('Frame.decodeMicE', () => { }); }); - describe('Messaging capability', () => { - it('should always set messaging to true for Mic-E', () => { - const data = 'CALL>4ABCDE:`c.l+@&\'/\'"G:}'; + describe("Messaging capability", () => { + it("should always set messaging to true for Mic-E", () => { + const data = "CALL>4ABCDE:`c.l+@&'/'\"G:}"; const frame = Frame.fromString(data); const decoded = frame.decode() as Payload; expect(decoded).not.toBeNull(); - if (decoded && decoded.type === 'position') { + if (decoded && decoded.type === "position") { expect(decoded.messaging).toBe(true); } }); }); }); -describe('Packet dissection with sections', () => { - it('should emit routing sections when emitSections is true', () => { - const data = 'KB1ABC-5>APRS,WIDE1-1,WIDE2-2*:!4903.50N/07201.75W-Test'; +describe("Packet dissection with sections", () => { + it("should emit routing sections when emitSections is true", () => { + const data = "KB1ABC-5>APRS,WIDE1-1,WIDE2-2*:!4903.50N/07201.75W-Test"; const frame = Frame.fromString(data); - const result = frame.decode(true) as { payload: Payload; structure: PacketStructure }; + const result = frame.decode(true) as { + payload: Payload; + structure: Dissected; + }; - expect(result).toHaveProperty('payload'); - expect(result).toHaveProperty('structure'); + expect(result).toHaveProperty("payload"); + expect(result).toHaveProperty("structure"); expect(result.structure).toBeDefined(); expect(result.structure.length).toBeGreaterThan(0); - const routingSection = result.structure.find(s => s.name === 'Routing'); + const routingSection = result.structure.find((s) => s.name === "Routing"); expect(routingSection).toBeDefined(); expect(routingSection?.fields).toBeDefined(); expect(routingSection?.fields?.length).toBeGreaterThan(0); - const sourceField = routingSection?.fields?.find(a => a.name === 'Source address'); + const sourceField = routingSection?.fields?.find( + (a) => a.name === "Source address", + ); expect(sourceField).toBeDefined(); - expect(sourceField?.size).toBeGreaterThan(0); + expect(sourceField?.length).toBeGreaterThan(0); - const destField = routingSection?.fields?.find(a => a.name === 'Destination address'); + const destField = routingSection?.fields?.find( + (a) => a.name === "Destination address", + ); expect(destField).toBeDefined(); - expect(destField?.size).toBeGreaterThan(0); + expect(destField?.length).toBeGreaterThan(0); }); - it('should emit position payload sections when emitSections is true', () => { - const data = 'CALL>APRS:!4903.50N/07201.75W-Test message'; + it("should emit position payload sections when emitSections is true", () => { + const data = "CALL>APRS:!4903.50N/07201.75W-Test message"; const frame = Frame.fromString(data); - const result = frame.decode(true) as { payload: Payload; structure: PacketStructure }; + const result = frame.decode(true) as { + payload: Payload; + structure: Dissected; + }; expect(result.payload).not.toBeNull(); - expect(result.payload?.type).toBe('position'); + expect(result.payload?.type).toBe("position"); expect(result.structure).toBeDefined(); expect(result.structure?.length).toBeGreaterThan(0); - const positionSection = result.structure?.find(s => s.name === 'position'); + const positionSection = result.structure?.find( + (s) => s.name === "position", + ); expect(positionSection).toBeDefined(); - expect(positionSection?.data.length).toBe(19); + expect(positionSection?.data?.byteLength).toBe(19); expect(positionSection?.fields).toBeDefined(); expect(positionSection?.fields?.length).toBeGreaterThan(0); }); - it('should not emit sections when emitSections is false or omitted', () => { - const data = 'CALL>APRS:!4903.50N/07201.75W-Test'; + it("should not emit sections when emitSections is false or omitted", () => { + const data = "CALL>APRS:!4903.50N/07201.75W-Test"; const frame = Frame.fromString(data); const result = frame.decode() as Payload; expect(result).not.toBeNull(); - expect(result?.type).toBe('position'); + expect(result?.type).toBe("position"); + // eslint-disable-next-line @typescript-eslint/no-explicit-any expect((result as any).sections).toBeUndefined(); }); - it('should emit timestamp section when present', () => { - const data = 'CALL>APRS:@092345z4903.50N/07201.75W>'; + it("should emit timestamp section when present", () => { + const data = "CALL>APRS:@092345z4903.50N/07201.75W>"; const frame = Frame.fromString(data); - const result = frame.decode(true) as { payload: Payload; structure: PacketStructure }; + const result = frame.decode(true) as { + payload: Payload; + structure: Dissected; + }; - expect(result.payload?.type).toBe('position'); + expect(result.payload?.type).toBe("position"); - const timestampSection = result.structure?.find(s => s.name === 'timestamp'); + const timestampSection = result.structure?.find( + (s) => s.name === "timestamp", + ); expect(timestampSection).toBeDefined(); - expect(timestampSection?.data.length).toBe(7); - expect(timestampSection?.fields?.map(a => a.name)).toEqual([ - 'day (DD)', - 'hour (HH)', - 'minute (MM)', - 'timezone indicator', + expect(timestampSection?.data?.byteLength).toBe(7); + expect(timestampSection?.fields?.map((a) => a.name)).toEqual([ + "day (DD)", + "hour (HH)", + "minute (MM)", + "timezone indicator", ]); }); - it('should emit compressed position sections', () => { + it("should emit compressed position sections", () => { const data = 'NOCALL-1>APRS:@092345z/:*E";qZ=OMRC/A=088132Hello World!'; const frame = Frame.fromString(data); - const result = frame.decode(true) as { payload: Payload; structure: PacketStructure }; + const result = frame.decode(true) as { + payload: Payload; + structure: Dissected; + }; - expect(result.payload?.type).toBe('position'); + expect(result.payload?.type).toBe("position"); - const positionSection = result.structure?.find(s => s.name === 'position'); + const positionSection = result.structure?.find( + (s) => s.name === "position", + ); expect(positionSection).toBeDefined(); - expect(positionSection?.data.length).toBe(13); + expect(positionSection?.data?.byteLength).toBe(13); - const latAttr = positionSection?.fields?.find(a => a.name === 'latitude'); + const latAttr = positionSection?.fields?.find((a) => a.name === "latitude"); expect(latAttr).toBeDefined(); expect(latAttr?.type).toBe(FieldType.STRING); }); - it('should emit comment section', () => { - const data = 'CALL>APRS:!4903.50N/07201.75W-Test message'; + it("should emit comment section", () => { + const data = "CALL>APRS:!4903.50N/07201.75W-Test message"; const frame = Frame.fromString(data); - const result = frame.decode(true) as { payload: Payload; structure: PacketStructure }; + const result = frame.decode(true) as { + payload: Payload; + structure: Dissected; + }; - expect(result.payload?.type).toBe('position'); - const commentSection = result.structure?.find(s => s.name === 'comment'); + expect(result.payload?.type).toBe("position"); + const commentSection = result.structure?.find((s) => s.name === "comment"); expect(commentSection).toBeDefined(); - expect(commentSection?.data.length).toBe('Test message'.length); - expect(commentSection?.fields?.[0]?.name).toBe('text'); + expect(commentSection?.data?.byteLength).toBe("Test message".length); + expect(commentSection?.fields?.[0]?.name).toBe("text"); }); }); -describe('Frame.decodeMessage', () => { - it('decodes a standard APRS message with 9-char recipient field', () => { - const raw = 'W1AW>APRS::KB1ABC-5 :Hello World'; +describe("Frame.decodeMessage", () => { + it("decodes a standard APRS message with 9-char recipient field", () => { + const raw = "W1AW>APRS::KB1ABC-5 :Hello World"; const frame = Frame.fromString(raw); - const decoded = frame.decode() as any; + const decoded = frame.decode() as MessagePayload; expect(decoded).not.toBeNull(); - expect(decoded?.type).toBe('message'); - expect(decoded?.to).toBe('KB1ABC-5'); - expect(decoded?.text).toBe('Hello World'); + expect(decoded?.type).toBe("message"); + expect(decoded?.addressee).toBe("KB1ABC-5"); + expect(decoded?.text).toBe("Hello World"); }); - it('emits recipient and text sections when emitSections is true', () => { - const raw = 'W1AW>APRS::KB1ABC :Test via spec'; + it("emits recipient and text sections when emitSections is true", () => { + const raw = "W1AW>APRS::KB1ABC :Test via spec"; const frame = Frame.fromString(raw); - const res = frame.decode(true) as { payload: any; structure: PacketStructure }; + const res = frame.decode(true) as { + payload: MessagePayload; + structure: Dissected; + }; expect(res.payload).not.toBeNull(); - expect(res.payload.type).toBe('message'); - const recipientSection = res.structure.find(s => s.name === 'recipient'); - const textSection = res.structure.find(s => s.name === 'text'); + expect(res.payload.type).toBe("message"); + const recipientSection = res.structure.find((s) => s.name === "recipient"); + const textSection = res.structure.find((s) => s.name === "text"); expect(recipientSection).toBeDefined(); expect(textSection).toBeDefined(); - expect(new TextDecoder().decode(recipientSection!.data).trim()).toBe('KB1ABC'); - expect(new TextDecoder().decode(textSection!.data)).toBe('Test via spec'); + expect(new TextDecoder().decode(recipientSection!.data).trim()).toBe( + "KB1ABC", + ); + expect(new TextDecoder().decode(textSection!.data)).toBe("Test via spec"); }); }); -describe('Frame.decoding: object and status', () => { - it('decodes an object payload (uncompressed position + comment)', () => { - const raw = 'N0CALL>APRS:;OBJECT001*120912z4903.50N/07201.75W>Test object'; +describe("Frame.decoding: object and status", () => { + it("decodes an object payload (uncompressed position + comment)", () => { + const raw = "N0CALL>APRS:;OBJECT001*120912z4903.50N/07201.75W>Test object"; const frame = Frame.parse(raw); - const res = frame.decode(true) as { payload: Payload | null; structure: PacketStructure }; - expect(res).toHaveProperty('payload'); + const res = frame.decode(true) as { + payload: Payload | null; + structure: Dissected; + }; + expect(res).toHaveProperty("payload"); expect(res.payload).not.toBeNull(); - if (res.payload?.type !== 'object') throw new Error('expected object payload'); + if (res.payload?.type !== "object") + throw new Error("expected object payload"); const payload = res.payload as ObjectPayload & { timestamp?: ITimestamp }; - expect(payload.name).toBe('OBJECT001'); + expect(payload.name).toBe("OBJECT001"); expect(payload.alive).toBe(true); expect(payload.timestamp).toBeDefined(); @@ -932,27 +1030,31 @@ describe('Frame.decoding: object and status', () => { expect(payload.position.latitude).toBeCloseTo(49.058333, 4); expect(payload.position.longitude).toBeCloseTo(-72.029166, 4); - expect(payload.position.comment).toBe('Test object'); + expect(payload.position.comment).toBe("Test object"); }); - it('decodes a status payload with timestamp and Maidenhead', () => { - const raw = 'N0CALL>APRS:>120912zTesting status FN20'; + it("decodes a status payload with timestamp and Maidenhead", () => { + const raw = "N0CALL>APRS:>120912zTesting status FN20"; const frame = Frame.parse(raw); - const res = frame.decode(true) as { payload: Payload | null; structure: PacketStructure }; - expect(res).toHaveProperty('payload'); + const res = frame.decode(true) as { + payload: Payload | null; + structure: Dissected; + }; + expect(res).toHaveProperty("payload"); expect(res.payload).not.toBeNull(); - if (res.payload?.type !== 'status') throw new Error('expected status payload'); + if (res.payload?.type !== "status") + throw new Error("expected status payload"); const payload = res.payload as StatusPayload & { timestamp?: ITimestamp }; - expect(payload.type).toBe('status'); + expect(payload.type).toBe("status"); expect(payload.timestamp).toBeDefined(); expect(payload.timestamp?.day).toBe(12); - expect(payload.text).toBe('Testing status'); - expect(payload.maidenhead).toBe('FN20'); + expect(payload.text).toBe("Testing status"); + expect(payload.maidenhead).toBe("FN20"); }); }); diff --git a/test/parser.test.ts b/test/parser.test.ts index 000c3f5..7fa82c1 100644 --- a/test/parser.test.ts +++ b/test/parser.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect } from "vitest"; import { base91ToNumber, knotsToKmh, @@ -7,79 +7,79 @@ import { metersToFeet, celsiusToFahrenheit, fahrenheitToCelsius, -} from '../src/parser'; +} from "../src/parser"; -describe('parser utilities', () => { - describe('base91ToNumber', () => { - it('decodes all-! to 0', () => { - expect(base91ToNumber('!!!!')).toBe(0); +describe("parser utilities", () => { + describe("base91ToNumber", () => { + it("decodes all-! to 0", () => { + expect(base91ToNumber("!!!!")).toBe(0); }); - it('decodes single character correctly', () => { + it("decodes single character correctly", () => { // 'A' === 65, digit = 65 - 33 = 32 - expect(base91ToNumber('A')).toBe(32); + expect(base91ToNumber("A")).toBe(32); }); - it('should decode multiple Base91 characters', () => { + it("should decode multiple Base91 characters", () => { // "!!" = 0 * 91 + 0 = 0 - expect(base91ToNumber('!!')).toBe(0); + expect(base91ToNumber("!!")).toBe(0); // "!#" = 0 * 91 + 2 = 2 - expect(base91ToNumber('!#')).toBe(2); + expect(base91ToNumber("!#")).toBe(2); // "#!" = 2 * 91 + 0 = 182 - expect(base91ToNumber('#!')).toBe(182); + expect(base91ToNumber("#!")).toBe(182); // "##" = 2 * 91 + 2 = 184 - expect(base91ToNumber('##')).toBe(184); + expect(base91ToNumber("##")).toBe(184); }); - it('should decode 4-character Base91 strings (used in APRS)', () => { + it("should decode 4-character Base91 strings (used in APRS)", () => { // Test with printable ASCII Base91 characters (33-123) - const testValue = base91ToNumber('!#%\''); + const testValue = base91ToNumber("!#%'"); expect(testValue).toBeGreaterThan(0); expect(testValue).toBeLessThan(91 * 91 * 91 * 91); }); - it('should decode maximum valid Base91 value', () => { + it("should decode maximum valid Base91 value", () => { // Maximum is '{' (ASCII 123, digit 90) repeated - const maxValue = base91ToNumber('{{{{'); + const maxValue = base91ToNumber("{{{{"); const expected = 90 * 91 * 91 * 91 + 90 * 91 * 91 + 90 * 91 + 90; expect(maxValue).toBe(expected); }); - it('should handle APRS compressed position example', () => { + it("should handle APRS compressed position example", () => { // Using actual characters from APRS test vector - const latStr = '/:*E'; - const lonStr = 'qZ=O'; + const latStr = "/:*E"; + const lonStr = "qZ=O"; const latValue = base91ToNumber(latStr); const lonValue = base91ToNumber(lonStr); // Just verify they decode without error and produce valid numbers - expect(typeof latValue).toBe('number'); - expect(typeof lonValue).toBe('number'); + expect(typeof latValue).toBe("number"); + expect(typeof lonValue).toBe("number"); expect(latValue).toBeGreaterThanOrEqual(0); expect(lonValue).toBeGreaterThanOrEqual(0); }); - it('throws on invalid character', () => { - expect(() => base91ToNumber(' ')).toThrow(); // space (code 32) is invalid + it("throws on invalid character", () => { + expect(() => base91ToNumber(" ")).toThrow(); // space (code 32) is invalid }); }); - describe('unit conversions', () => { - it('converts knots <-> km/h', () => { + describe("unit conversions", () => { + it("converts knots <-> km/h", () => { expect(knotsToKmh(10)).toBeCloseTo(18.52, 5); expect(kmhToKnots(18.52)).toBeCloseTo(10, 3); }); - it('converts feet <-> meters', () => { + it("converts feet <-> meters", () => { expect(feetToMeters(10)).toBeCloseTo(3.048, 6); expect(metersToFeet(3.048)).toBeCloseTo(10, 6); }); - it('converts celsius <-> fahrenheit', () => { + it("converts celsius <-> fahrenheit", () => { expect(celsiusToFahrenheit(0)).toBeCloseTo(32, 6); expect(fahrenheitToCelsius(32)).toBeCloseTo(0, 6); expect(celsiusToFahrenheit(100)).toBeCloseTo(212, 6);