From 54f6ddfedd4cde54d75622722e89440db46d1d25 Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Sun, 8 Mar 2026 11:34:05 -0400 Subject: [PATCH] refactor: code quality improvements across the board --- .github/workflows/ci.yml | 20 +- .gitignore | 7 + .npmrc | 1 - .prettierrc | 6 + eslint.config.mjs | 12 + package.json | 17 +- pnpm-lock.yaml | 733 +++++++++++++++++++++++++++++++++++++++ src/index.ts | 84 ++++- src/types.ts | 26 +- test-cjs.cjs | 133 +++---- test.mjs | 575 +++++++++++++++--------------- tsconfig.json | 2 + 12 files changed, 1254 insertions(+), 362 deletions(-) create mode 100644 .prettierrc create mode 100644 eslint.config.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b843cc8..21f4ffe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,10 +32,10 @@ jobs: run: pnpm run build - name: Run tests (ESM) - run: node test.mjs + run: node --test test.mjs - name: Run tests (CJS) - run: node test-cjs.cjs + run: node --test test-cjs.cjs typecheck: runs-on: ubuntu-latest @@ -56,6 +56,22 @@ jobs: - run: pnpm install --frozen-lockfile - run: pnpm run typecheck + lint: + name: Lint & Format + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 10 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm run lint + - run: pnpm run format:check + pack-check: runs-on: ubuntu-latest permissions: diff --git a/.gitignore b/.gitignore index a969533..a27d05f 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,10 @@ dist/ /bin/spa_cli /bin/*.c /bin/*.h +.vscode/* +.idea/ +.codex/ +.aider/ +.aider.chat.history.md +.continue/ +.gemini/ diff --git a/.npmrc b/.npmrc index 8b13789..e69de29 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +0,0 @@ - diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..383f607 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "trailingComma": "all", + "printWidth": 100, + "tabWidth": 2 +} diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..bcb5509 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,12 @@ +import eslint from '@eslint/js'; +import tseslint from 'typescript-eslint'; +import eslintConfigPrettier from 'eslint-config-prettier'; + +export default tseslint.config( + eslint.configs.recommended, + ...tseslint.configs.recommended, + eslintConfigPrettier, + { + ignores: ['dist/', 'node_modules/', 'test.mjs', 'test-cjs.cjs', 'lib/'], + }, +); diff --git a/package.json b/package.json index 91475dd..d1eebb9 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,10 @@ }, "sideEffects": false, "files": [ - "dist/", + "dist/index.cjs", + "dist/index.mjs", + "dist/index.d.ts", + "dist/index.d.mts", "lib/", "README.md", "CHANGELOG.md", @@ -31,7 +34,10 @@ "build": "tsup", "typecheck": "tsc --noEmit", "pretest": "tsup", - "test": "node test.mjs && node test-cjs.cjs", + "test": "node --test test.mjs && node --test test-cjs.cjs", + "lint": "eslint src/", + "format": "prettier --write src/", + "format:check": "prettier --check src/", "prepublishOnly": "tsup" }, "keywords": [ @@ -64,8 +70,13 @@ "registry": "https://registry.npmjs.org/" }, "devDependencies": { + "@eslint/js": "^10.0.1", "@types/node": "^25.3.0", + "eslint": "^10.0.3", + "eslint-config-prettier": "^10.1.8", + "prettier": "^3.8.1", "tsup": "^8.5.1", - "typescript": "^5.9.3" + "typescript": "^5.9.3", + "typescript-eslint": "^8.56.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6f13fe1..314488d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,15 +8,30 @@ importers: .: devDependencies: + '@eslint/js': + specifier: ^10.0.1 + version: 10.0.1(eslint@10.0.3) '@types/node': specifier: ^25.3.0 version: 25.3.0 + eslint: + specifier: ^10.0.3 + version: 10.0.3 + eslint-config-prettier: + specifier: ^10.1.8 + version: 10.1.8(eslint@10.0.3) + prettier: + specifier: ^3.8.1 + version: 3.8.1 tsup: specifier: ^8.5.1 version: 8.5.1(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 + typescript-eslint: + specifier: ^8.56.1 + version: 8.56.1(eslint@10.0.3)(typescript@5.9.3) packages: @@ -176,6 +191,61 @@ packages: cpu: [x64] os: [win32] + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.23.3': + resolution: {integrity: sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/config-helpers@0.5.3': + resolution: {integrity: sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@1.1.1': + resolution: {integrity: sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/js@10.0.1': + resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/object-schema@3.0.3': + resolution: {integrity: sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.6.1': + resolution: {integrity: sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -327,20 +397,101 @@ packages: cpu: [x64] os: [win32] + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/node@25.3.0': resolution: {integrity: sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==} + '@typescript-eslint/eslint-plugin@8.56.1': + resolution: {integrity: sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.56.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.56.1': + resolution: {integrity: sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.56.1': + resolution: {integrity: sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.56.1': + resolution: {integrity: sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.56.1': + resolution: {integrity: sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.56.1': + resolution: {integrity: sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.56.1': + resolution: {integrity: sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.56.1': + resolution: {integrity: sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.56.1': + resolution: {integrity: sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.56.1': + resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + acorn@8.16.0: resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} engines: {node: '>=0.4.0'} hasBin: true + ajv@6.14.0: + resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + brace-expansion@5.0.4: + resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} + engines: {node: 18 || 20 || >=22} + bundle-require@5.1.0: resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -366,6 +517,10 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -375,11 +530,75 @@ packages: supports-color: optional: true + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + esbuild@0.27.3: resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} engines: {node: '>=18'} hasBin: true + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@10.0.3: + resolution: {integrity: sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -389,18 +608,76 @@ packages: picomatch: optional: true + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + fix-dts-default-cjs-exports@1.0.1: resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.4: + resolution: {integrity: sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -412,9 +689,17 @@ packages: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + minimatch@10.2.4: + resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} + engines: {node: 18 || 20 || >=22} + mlly@1.8.0: resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} @@ -424,10 +709,33 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -463,6 +771,19 @@ packages: yaml: optional: true + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@3.8.1: + resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} + engines: {node: '>=14'} + hasBin: true + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} @@ -476,6 +797,19 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + source-map@0.7.6: resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} engines: {node: '>= 12'} @@ -503,6 +837,12 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true + ts-api-utils@2.4.0: + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} @@ -525,6 +865,17 @@ packages: typescript: optional: true + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typescript-eslint@8.56.1: + resolution: {integrity: sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -536,6 +887,22 @@ packages: undici-types@7.18.2: resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + snapshots: '@esbuild/aix-ppc64@0.27.3': @@ -616,6 +983,51 @@ snapshots: '@esbuild/win32-x64@0.27.3': optional: true + '@eslint-community/eslint-utils@4.9.1(eslint@10.0.3)': + dependencies: + eslint: 10.0.3 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.23.3': + dependencies: + '@eslint/object-schema': 3.0.3 + debug: 4.4.3 + minimatch: 10.2.4 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.5.3': + dependencies: + '@eslint/core': 1.1.1 + + '@eslint/core@1.1.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/js@10.0.1(eslint@10.0.3)': + optionalDependencies: + eslint: 10.0.3 + + '@eslint/object-schema@3.0.3': {} + + '@eslint/plugin-kit@0.6.1': + dependencies: + '@eslint/core': 1.1.1 + levn: 0.4.1 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -705,16 +1117,128 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.59.0': optional: true + '@types/esrecurse@4.3.1': {} + '@types/estree@1.0.8': {} + '@types/json-schema@7.0.15': {} + '@types/node@25.3.0': dependencies: undici-types: 7.18.2 + '@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@10.0.3)(typescript@5.9.3))(eslint@10.0.3)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.56.1(eslint@10.0.3)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.56.1 + '@typescript-eslint/type-utils': 8.56.1(eslint@10.0.3)(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@10.0.3)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.56.1 + eslint: 10.0.3 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.56.1(eslint@10.0.3)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.56.1 + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.56.1 + debug: 4.4.3 + eslint: 10.0.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.56.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) + '@typescript-eslint/types': 8.56.1 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.56.1': + dependencies: + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/visitor-keys': 8.56.1 + + '@typescript-eslint/tsconfig-utils@8.56.1(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.56.1(eslint@10.0.3)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@10.0.3)(typescript@5.9.3) + debug: 4.4.3 + eslint: 10.0.3 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.56.1': {} + + '@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.56.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/visitor-keys': 8.56.1 + debug: 4.4.3 + minimatch: 10.2.4 + semver: 7.7.4 + tinyglobby: 0.2.15 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.56.1(eslint@10.0.3)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3) + '@typescript-eslint/scope-manager': 8.56.1 + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + eslint: 10.0.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.56.1': + dependencies: + '@typescript-eslint/types': 8.56.1 + eslint-visitor-keys: 5.0.1 + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + acorn@8.16.0: {} + ajv@6.14.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + any-promise@1.3.0: {} + balanced-match@4.0.4: {} + + brace-expansion@5.0.4: + dependencies: + balanced-match: 4.0.4 + bundle-require@5.1.0(esbuild@0.27.3): dependencies: esbuild: 0.27.3 @@ -732,10 +1256,18 @@ snapshots: consola@3.4.2: {} + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + debug@4.4.3: dependencies: ms: 2.1.3 + deep-is@0.1.4: {} + esbuild@0.27.3: optionalDependencies: '@esbuild/aix-ppc64': 0.27.3 @@ -765,31 +1297,164 @@ snapshots: '@esbuild/win32-ia32': 0.27.3 '@esbuild/win32-x64': 0.27.3 + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@10.1.8(eslint@10.0.3): + dependencies: + eslint: 10.0.3 + + eslint-scope@9.1.2: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.8 + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@10.0.3: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.23.3 + '@eslint/config-helpers': 0.5.3 + '@eslint/core': 1.1.1 + '@eslint/plugin-kit': 0.6.1 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.14.0 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + minimatch: 10.2.4 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@11.2.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 5.0.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + fix-dts-default-cjs-exports@1.0.1: dependencies: magic-string: 0.30.21 mlly: 1.8.0 rollup: 4.59.0 + flat-cache@4.0.1: + dependencies: + flatted: 3.3.4 + keyv: 4.5.4 + + flatted@3.3.4: {} + fsevents@2.3.3: optional: true + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + imurmurhash@0.1.4: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + isexe@2.0.0: {} + joycon@3.1.1: {} + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} load-tsconfig@0.2.5: {} + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + minimatch@10.2.4: + dependencies: + brace-expansion: 5.0.4 + mlly@1.8.0: dependencies: acorn: 8.16.0 @@ -805,8 +1470,31 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 + natural-compare@1.4.0: {} + object-assign@4.1.1: {} + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + pathe@2.0.3: {} picocolors@1.1.1: {} @@ -825,6 +1513,12 @@ snapshots: dependencies: lilconfig: 3.1.3 + prelude-ls@1.2.1: {} + + prettier@3.8.1: {} + + punycode@2.3.1: {} + readdirp@4.1.2: {} resolve-from@5.0.0: {} @@ -860,6 +1554,14 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.59.0 fsevents: 2.3.3 + semver@7.7.4: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + source-map@0.7.6: {} sucrase@3.35.1: @@ -889,6 +1591,10 @@ snapshots: tree-kill@1.2.2: {} + ts-api-utils@2.4.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + ts-interface-checker@0.1.13: {} tsup@8.5.1(typescript@5.9.3): @@ -918,8 +1624,35 @@ snapshots: - tsx - yaml + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript-eslint@8.56.1(eslint@10.0.3)(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.56.1(eslint@10.0.3)(typescript@5.9.3))(eslint@10.0.3)(typescript@5.9.3) + '@typescript-eslint/parser': 8.56.1(eslint@10.0.3)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@10.0.3)(typescript@5.9.3) + eslint: 10.0.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + typescript@5.9.3: {} ufo@1.6.3: {} undici-types@7.18.2: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + yocto-queue@0.1.0: {} diff --git a/src/index.ts b/src/index.ts index c97ee3c..1978c0f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,6 +21,9 @@ import type { SpaFormattedAnglesResult, } from './types.js'; +/** Degrees-to-radians conversion factor. */ +const DEG = Math.PI / 180; + // The core SPA algorithm lives in lib/spa.js (the JS port of the NREL C source). // In ESM builds, tsup injects a createRequire-based __require shim via the banner // option (see tsup.config.ts). In CJS builds, require() is natively available. @@ -76,6 +79,9 @@ function assertFiniteNumber(value: unknown, name: string): asserts value is numb /** * Format fractional hours to HH:MM:SS string. * Returns "N/A" for non-finite or negative values (polar night/day scenarios). + * + * @param hours - Fractional hours (e.g., 12.5 for 12:30:00) + * @returns Formatted time string in HH:MM:SS format, or "N/A" */ export function formatTime(hours: number): string { if (!isFinite(hours) || hours < 0) return 'N/A'; @@ -88,9 +94,7 @@ export function formatTime(hours: number): string { const s = rem - m * 60; return ( - String(h).padStart(2, '0') + ':' + - String(m).padStart(2, '0') + ':' + - String(s).padStart(2, '0') + String(h).padStart(2, '0') + ':' + String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0') ); } @@ -108,18 +112,16 @@ function adjustForCustomAngle( base: SpaDataInstance, zenithAngle: number, ): { sunrise: number; sunset: number } { - const phi = base.latitude * Math.PI / 180; - const delta = base.delta * Math.PI / 180; - const Z = zenithAngle * Math.PI / 180; - const cosH0 = - (Math.cos(Z) - Math.sin(phi) * Math.sin(delta)) / - (Math.cos(phi) * Math.cos(delta)); + const phi = base.latitude * DEG; + const delta = base.delta * DEG; + const Z = zenithAngle * DEG; + const cosH0 = (Math.cos(Z) - Math.sin(phi) * Math.sin(delta)) / (Math.cos(phi) * Math.cos(delta)); if (cosH0 < -1 || cosH0 > 1) { return { sunrise: NaN, sunset: NaN }; } - const H0h = (Math.acos(cosH0) * 180 / Math.PI) / 15; + const H0h = Math.acos(cosH0) / DEG / 15; return { sunrise: base.suntransit - H0h, sunset: base.suntransit + H0h, @@ -135,6 +137,8 @@ function adjustForCustomAngle( * @param timezone - Hours from UTC (e.g., -4 for EDT). Default: 0 * @param options - Optional atmospheric and calculation parameters * @returns Solar position result with raw numerical values + * @throws {TypeError} If date, latitude, longitude, timezone, or options numeric fields are not finite numbers + * @throws {RangeError} If latitude, longitude, timezone, function code, or angle values are out of range */ export function getSpa( date: Date, @@ -184,8 +188,28 @@ export function getSpa( } const tz = timezone ?? 0; + assertFiniteNumber(tz, 'timezone'); + if (tz < -18 || tz > 18) { + throw new RangeError(`SPA: timezone must be between -18 and 18, got ${tz}`); + } + const opts = options ?? {}; + const optNumericFields = [ + 'elevation', + 'pressure', + 'temperature', + 'delta_t', + 'slope', + 'azm_rotation', + 'atmos_refract', + ] as const; + for (const field of optNumericFields) { + if (opts[field] !== undefined) { + assertFiniteNumber(opts[field], `options.${field}`); + } + } + const fnCode = opts.function ?? SPA_ZA_RTS; if (fnCode !== 0 && fnCode !== 1 && fnCode !== 2 && fnCode !== 3) { throw new RangeError( @@ -193,6 +217,21 @@ export function getSpa( ); } + // Validate custom angle values before checking function code compatibility. + if (angles && angles.length > 0) { + for (let i = 0; i < angles.length; i++) { + const a = angles[i]; + if (typeof a !== 'number' || !isFinite(a)) { + throw new TypeError( + `SPA: angles[${i}] must be a finite number, got ${typeof a === 'number' ? a : typeof a}`, + ); + } + if (a < 0 || a > 180) { + throw new RangeError(`SPA: angles[${i}] must be between 0 and 180, got ${a}`); + } + } + } + // Custom angle calculations depend on suntransit, which requires an RTS function code. if (angles && angles.length > 0 && fnCode !== 2 && fnCode !== 3) { throw new RangeError( @@ -252,6 +291,9 @@ export function getSpa( /** * Same as getSpa(), but formats sunrise, solarNoon, and sunset as HH:MM:SS strings. * Returns "N/A" for time fields during polar day or polar night. + * + * @throws {TypeError} If date, latitude, longitude, timezone, or options numeric fields are not finite numbers + * @throws {RangeError} If latitude, longitude, timezone, function code, or angle values are out of range */ export function calcSpa( date: Date, @@ -263,6 +305,9 @@ export function calcSpa( /** * Same as getSpa() with custom angles, but formats all time values as HH:MM:SS strings. * Returns "N/A" for time fields during polar day or polar night. + * + * @throws {TypeError} If date, latitude, longitude, timezone, or options numeric fields are not finite numbers + * @throws {RangeError} If latitude, longitude, timezone, function code, or angle values are out of range */ export function calcSpa( date: Date, @@ -281,17 +326,26 @@ export function calcSpa( angles?: number[], ): SpaFormattedResult | SpaFormattedResultWithAngles { if (angles !== undefined && angles.length > 0) { - const raw = getSpa(date, latitude, longitude, timezone, options, angles as [number, ...number[]]); + const raw = getSpa( + date, + latitude, + longitude, + timezone, + options, + angles as [number, ...number[]], + ); return { zenith: raw.zenith, azimuth: raw.azimuth, sunrise: formatTime(raw.sunrise), solarNoon: formatTime(raw.solarNoon), sunset: formatTime(raw.sunset), - angles: raw.angles.map((a): SpaFormattedAnglesResult => ({ - sunrise: formatTime(a.sunrise), - sunset: formatTime(a.sunset), - })), + angles: raw.angles.map( + (a): SpaFormattedAnglesResult => ({ + sunrise: formatTime(a.sunrise), + sunset: formatTime(a.sunset), + }), + ), }; } diff --git a/src/types.ts b/src/types.ts index d233e32..5447ab0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,10 +1,32 @@ -/** SPA function codes. Control which outputs are computed. */ +/** + * Compute topocentric zenith and azimuth angles only. + * Does not compute sunrise, sunset, or solar noon. + */ export const SPA_ZA = 0 as const; + +/** + * Compute zenith, azimuth, and incidence angle for a tilted surface. + * Requires slope and azm_rotation in SpaOptions. + */ export const SPA_ZA_INC = 1 as const; + +/** + * Compute sunrise, sunset, and sun transit (solar noon) in addition to + * zenith and azimuth. This is the default function code. + */ export const SPA_ZA_RTS = 2 as const; + +/** + * Compute all outputs: zenith, azimuth, incidence angle, sunrise, sunset, + * and sun transit. Combines SPA_ZA_INC and SPA_ZA_RTS. + */ export const SPA_ALL = 3 as const; -export type SpaFunctionCode = typeof SPA_ZA | typeof SPA_ZA_INC | typeof SPA_ZA_RTS | typeof SPA_ALL; +export type SpaFunctionCode = + | typeof SPA_ZA + | typeof SPA_ZA_INC + | typeof SPA_ZA_RTS + | typeof SPA_ALL; export interface SpaOptions { /** Observer elevation in meters above sea level. Default: 0. */ diff --git a/test-cjs.cjs b/test-cjs.cjs index 84b4735..e41286f 100644 --- a/test-cjs.cjs +++ b/test-cjs.cjs @@ -1,5 +1,6 @@ 'use strict'; +const { describe, it } = require('node:test'); const assert = require('node:assert/strict'); const { getSpa, @@ -10,67 +11,77 @@ const { SPA_ALL, } = require('./dist/index.cjs'); -let passed = 0; -let failed = 0; - -function test(name, fn) { - try { - fn(); - console.log(`${name}... PASS`); - passed++; - } catch (err) { - console.error(`${name}... FAIL: ${err.message}`); - failed++; - } -} - -// ─── Exports ───────────────────────────────────────────────────────────────── - -test('CJS: getSpa is a function', () => assert.equal(typeof getSpa, 'function')); -test('CJS: calcSpa is a function', () => assert.equal(typeof calcSpa, 'function')); -test('CJS: formatTime is a function', () => assert.equal(typeof formatTime, 'function')); -test('CJS: SPA_ZA === 0', () => assert.equal(SPA_ZA, 0)); -test('CJS: SPA_ZA_RTS === 2', () => assert.equal(SPA_ZA_RTS, 2)); -test('CJS: SPA_ALL === 3', () => assert.equal(SPA_ALL, 3)); - -// ─── Correctness ────────────────────────────────────────────────────────────── - -const nyc = calcSpa( - new Date('2025-06-21T00:00:00Z'), - 40.7128, -74.006, -4, - { elevation: 10, pressure: 1013, temperature: 20 }, -); - -test('CJS: NYC sunrise = 05:25:03', () => assert.equal(nyc.sunrise, '05:25:03')); -test('CJS: NYC solarNoon = 12:57:56', () => assert.equal(nyc.solarNoon, '12:57:56')); -test('CJS: NYC sunset = 20:30:35', () => assert.equal(nyc.sunset, '20:30:35')); -test('CJS: zenith is number', () => assert.equal(typeof nyc.zenith, 'number')); -test('CJS: azimuth 0-360', () => assert.ok(nyc.azimuth >= 0 && nyc.azimuth <= 360)); - -// ─── formatTime ─────────────────────────────────────────────────────────────── - -test('CJS formatTime: noon', () => assert.equal(formatTime(12), '12:00:00')); -test('CJS formatTime: negative = N/A', () => assert.equal(formatTime(-1), 'N/A')); -test('CJS formatTime: NaN = N/A', () => assert.equal(formatTime(NaN), 'N/A')); - -// ─── Custom angles ──────────────────────────────────────────────────────────── - -const twilight = getSpa( - new Date('2025-06-21T00:00:00Z'), - 40.7128, -74.006, -4, - { elevation: 10 }, - [96, 102, 108], -); - -test('CJS angles: has angles array', () => assert.ok(Array.isArray(twilight.angles))); -test('CJS angles: three entries', () => assert.equal(twilight.angles.length, 3)); -test('CJS angles: civil < standard sunrise', () => { - const standard = getSpa(new Date('2025-06-21T00:00:00Z'), 40.7128, -74.006, -4, { elevation: 10 }); - assert.ok(twilight.angles[0].sunrise < standard.sunrise); +describe('CJS exports', () => { + it('getSpa is a function', () => assert.equal(typeof getSpa, 'function')); + it('calcSpa is a function', () => assert.equal(typeof calcSpa, 'function')); + it('formatTime is a function', () => assert.equal(typeof formatTime, 'function')); + it('SPA_ZA === 0', () => assert.equal(SPA_ZA, 0)); + it('SPA_ZA_RTS === 2', () => assert.equal(SPA_ZA_RTS, 2)); + it('SPA_ALL === 3', () => assert.equal(SPA_ALL, 3)); }); -// ─── Summary ───────────────────────────────────────────────────────────────── +describe('CJS correctness', () => { + const nyc = calcSpa( + new Date('2025-06-21T00:00:00Z'), + 40.7128, -74.006, -4, + { elevation: 10, pressure: 1013, temperature: 20 }, + ); -console.log('---'); -console.log(`${passed + failed} tests total: ${passed} passed, ${failed} failed`); -if (failed > 0) process.exit(1); + it('NYC sunrise = 05:25:03', () => assert.equal(nyc.sunrise, '05:25:03')); + it('NYC solarNoon = 12:57:56', () => assert.equal(nyc.solarNoon, '12:57:56')); + it('NYC sunset = 20:30:35', () => assert.equal(nyc.sunset, '20:30:35')); + it('zenith is number', () => assert.equal(typeof nyc.zenith, 'number')); + it('azimuth 0-360', () => assert.ok(nyc.azimuth >= 0 && nyc.azimuth <= 360)); +}); + +describe('CJS formatTime', () => { + it('noon', () => assert.equal(formatTime(12), '12:00:00')); + it('negative = N/A', () => assert.equal(formatTime(-1), 'N/A')); + it('NaN = N/A', () => assert.equal(formatTime(NaN), 'N/A')); +}); + +describe('CJS custom angles', () => { + const twilight = getSpa( + new Date('2025-06-21T00:00:00Z'), + 40.7128, -74.006, -4, + { elevation: 10 }, + [96, 102, 108], + ); + + it('has angles array', () => assert.ok(Array.isArray(twilight.angles))); + it('three entries', () => assert.equal(twilight.angles.length, 3)); + it('civil < standard sunrise', () => { + const standard = getSpa(new Date('2025-06-21T00:00:00Z'), 40.7128, -74.006, -4, { elevation: 10 }); + assert.ok(twilight.angles[0].sunrise < standard.sunrise); + }); +}); + +describe('CJS input validation', () => { + it('invalid latitude throws TypeError', () => { + assert.throws(() => getSpa(new Date(), 'bad', -74, 0), TypeError); + }); + it('NaN latitude throws TypeError', () => { + assert.throws(() => getSpa(new Date(), NaN, -74, 0), TypeError); + }); + it('Infinity longitude throws TypeError', () => { + assert.throws(() => getSpa(new Date(), 40, Infinity, 0), TypeError); + }); + it('timezone > 18 throws RangeError', () => { + assert.throws(() => getSpa(new Date(), 40, -74, 19), RangeError); + }); + it('timezone < -18 throws RangeError', () => { + assert.throws(() => getSpa(new Date(), 40, -74, -19), RangeError); + }); + it('NaN timezone throws TypeError', () => { + assert.throws(() => getSpa(new Date(), 40, -74, NaN), TypeError); + }); + it('non-finite option field throws TypeError', () => { + assert.throws(() => getSpa(new Date(), 40, -74, 0, { elevation: Infinity }), TypeError); + }); + it('angle out of range throws RangeError', () => { + assert.throws(() => getSpa(new Date(), 40, -74, 0, null, [200]), RangeError); + }); + it('NaN angle throws TypeError', () => { + assert.throws(() => getSpa(new Date(), 40, -74, 0, null, [NaN]), TypeError); + }); +}); diff --git a/test.mjs b/test.mjs index 814d0d0..c9f8550 100644 --- a/test.mjs +++ b/test.mjs @@ -1,3 +1,4 @@ +import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { getSpa, @@ -9,20 +10,6 @@ import { SPA_ALL, } from './dist/index.mjs'; -let passed = 0; -let failed = 0; - -function test(name, fn) { - try { - fn(); - console.log(`${name}... PASS`); - passed++; - } catch (err) { - console.error(`${name}... FAIL: ${err.message}`); - failed++; - } -} - function close(actual, expected, tolerance = 0.001, label = '') { assert.ok( Math.abs(actual - expected) <= tolerance, @@ -30,275 +17,307 @@ function close(actual, expected, tolerance = 0.001, label = '') { ); } -// ─── Exports ───────────────────────────────────────────────────────────────── - -test('exports: getSpa is a function', () => assert.equal(typeof getSpa, 'function')); -test('exports: calcSpa is a function', () => assert.equal(typeof calcSpa, 'function')); -test('exports: formatTime is a function', () => assert.equal(typeof formatTime, 'function')); -test('exports: SPA_ZA === 0', () => assert.equal(SPA_ZA, 0)); -test('exports: SPA_ZA_INC === 1', () => assert.equal(SPA_ZA_INC, 1)); -test('exports: SPA_ZA_RTS === 2', () => assert.equal(SPA_ZA_RTS, 2)); -test('exports: SPA_ALL === 3', () => assert.equal(SPA_ALL, 3)); - -// ─── formatTime ────────────────────────────────────────────────────────────── - -test('formatTime: zero', () => assert.equal(formatTime(0), '00:00:00')); -test('formatTime: noon', () => assert.equal(formatTime(12), '12:00:00')); -test('formatTime: 23:59:59', () => assert.equal(formatTime(23 + 59/60 + 59/3600), '23:59:59')); -test('formatTime: fractional rounding', () => assert.equal(formatTime(5.4174893), '05:25:03')); -test('formatTime: negative returns N/A', () => assert.equal(formatTime(-1), 'N/A')); -test('formatTime: NaN returns N/A', () => assert.equal(formatTime(NaN), 'N/A')); -test('formatTime: Infinity returns N/A', () => assert.equal(formatTime(Infinity), 'N/A')); -test('formatTime: midnight wrap (24h)', () => { - // A time of exactly 24.0 rounds to 00:00:00 - assert.equal(formatTime(24), '00:00:00'); +describe('exports', () => { + it('getSpa is a function', () => assert.equal(typeof getSpa, 'function')); + it('calcSpa is a function', () => assert.equal(typeof calcSpa, 'function')); + it('formatTime is a function', () => assert.equal(typeof formatTime, 'function')); + it('SPA_ZA === 0', () => assert.equal(SPA_ZA, 0)); + it('SPA_ZA_INC === 1', () => assert.equal(SPA_ZA_INC, 1)); + it('SPA_ZA_RTS === 2', () => assert.equal(SPA_ZA_RTS, 2)); + it('SPA_ALL === 3', () => assert.equal(SPA_ALL, 3)); }); -// ─── getSpa: New York summer solstice (validated against NREL C reference) ─── - -const NYC_SUMMER = getSpa( - new Date('2025-06-21T00:00:00Z'), - 40.7128, -74.006, -4, - { elevation: 10, pressure: 1013, temperature: 20 }, -); - -test('NYC summer: sunrise ~05:25:03', () => close(NYC_SUMMER.sunrise, 5.417, 0.001, 'sunrise')); -test('NYC summer: solarNoon ~12:57:56', () => close(NYC_SUMMER.solarNoon, 12.965, 0.001, 'solarNoon')); -test('NYC summer: sunset ~20:30:35', () => close(NYC_SUMMER.sunset, 20.509, 0.001, 'sunset')); -test('NYC summer: zenith is a number', () => assert.equal(typeof NYC_SUMMER.zenith, 'number')); -test('NYC summer: azimuth 0-360', () => { - assert.ok(NYC_SUMMER.azimuth >= 0 && NYC_SUMMER.azimuth <= 360, `azimuth ${NYC_SUMMER.azimuth}`); +describe('formatTime', () => { + it('zero', () => assert.equal(formatTime(0), '00:00:00')); + it('noon', () => assert.equal(formatTime(12), '12:00:00')); + it('23:59:59', () => assert.equal(formatTime(23 + 59/60 + 59/3600), '23:59:59')); + it('fractional rounding', () => assert.equal(formatTime(5.4174893), '05:25:03')); + it('negative returns N/A', () => assert.equal(formatTime(-1), 'N/A')); + it('NaN returns N/A', () => assert.equal(formatTime(NaN), 'N/A')); + it('Infinity returns N/A', () => assert.equal(formatTime(Infinity), 'N/A')); + it('midnight wrap (24h)', () => assert.equal(formatTime(24), '00:00:00')); }); -// ─── getSpa: New York winter solstice ──────────────────────────────────────── - -const NYC_WINTER = getSpa( - new Date('2025-12-21T00:00:00Z'), - 40.7128, -74.006, -5, - { elevation: 10, pressure: 1013, temperature: 5 }, -); - -test('NYC winter: sunrise ~07:16:41', () => close(NYC_WINTER.sunrise, 7.278, 0.001, 'sunrise')); -test('NYC winter: solarNoon ~11:54:19', () => close(NYC_WINTER.solarNoon, 11.905, 0.001, 'solarNoon')); -test('NYC winter: sunset ~16:31:56', () => close(NYC_WINTER.sunset, 16.532, 0.001, 'sunset')); - -// ─── getSpa: London summer ─────────────────────────────────────────────────── - -const LONDON_SUMMER = getSpa( - new Date('2025-06-21T00:00:00Z'), - 51.5074, -0.1278, 1, - { elevation: 11, pressure: 1013, temperature: 18 }, -); - -test('London summer: sunrise ~04:43:07', () => close(LONDON_SUMMER.sunrise, 4.718, 0.001, 'sunrise')); -test('London summer: sunset ~21:21:37', () => close(LONDON_SUMMER.sunset, 21.360, 0.001, 'sunset')); - -// ─── getSpa: Tokyo ─────────────────────────────────────────────────────────── - -const TOKYO_SUMMER = getSpa( - new Date('2025-06-21T00:00:00Z'), - 35.6895, 139.6917, 9, - { elevation: 40, pressure: 1013, temperature: 22 }, -); - -test('Tokyo summer: sunrise ~04:25:52', () => close(TOKYO_SUMMER.sunrise, 4.431, 0.001, 'sunrise')); -test('Tokyo summer: sunset ~19:00:22', () => close(TOKYO_SUMMER.sunset, 19.006, 0.001, 'sunset')); - -// ─── getSpa: Sydney winter (southern hemisphere) ───────────────────────────── - -const SYDNEY_WINTER = getSpa( - new Date('2025-06-21T00:00:00Z'), - -33.8688, 151.2093, 10, - { elevation: 58, pressure: 1013, temperature: 15 }, -); - -test('Sydney winter: sunrise ~07:00:12', () => close(SYDNEY_WINTER.sunrise, 7.003, 0.001, 'sunrise')); -test('Sydney winter: sunset ~16:53:52', () => close(SYDNEY_WINTER.sunset, 16.898, 0.001, 'sunset')); - -// ─── getSpa: Quito (equator, equinox) ──────────────────────────────────────── - -const QUITO_EQUINOX = getSpa( - new Date('2025-03-20T00:00:00Z'), - -0.1807, -78.4678, -5, - { elevation: 2850, pressure: 789, temperature: 14 }, -); - -test('Quito equinox: sunrise ~06:17:54', () => close(QUITO_EQUINOX.sunrise, 6.298, 0.001, 'sunrise')); -test('Quito equinox: sunset ~18:24:25', () => close(QUITO_EQUINOX.sunset, 18.407, 0.001, 'sunset')); - -// ─── getSpa: polar night (Tromso, arctic winter) ───────────────────────────── - -const TROMSO_POLAR = getSpa( - new Date('2025-12-21T00:00:00Z'), - 69.6492, 18.9553, 1, - { elevation: 0, pressure: 1013, temperature: -2 }, -); - -// NREL sets sunrise/sunset to -99999 when the sun never rises. -test('Tromso polar: sunrise < 0 (polar night sentinel)', () => assert.ok(TROMSO_POLAR.sunrise < 0)); -test('Tromso polar: zenith > 90 (sun below horizon)', () => assert.ok(TROMSO_POLAR.zenith > 90)); - -// ─── calcSpa: formatted output ─────────────────────────────────────────────── - -const NYC_FMT = calcSpa( - new Date('2025-06-21T00:00:00Z'), - 40.7128, -74.006, -4, - { elevation: 10, pressure: 1013, temperature: 20 }, -); - -test('calcSpa: sunrise is string', () => assert.equal(typeof NYC_FMT.sunrise, 'string')); -test('calcSpa: solarNoon is string', () => assert.equal(typeof NYC_FMT.solarNoon, 'string')); -test('calcSpa: sunset is string', () => assert.equal(typeof NYC_FMT.sunset, 'string')); -test('calcSpa: sunrise format HH:MM:SS', () => assert.match(NYC_FMT.sunrise, /^\d{2}:\d{2}:\d{2}$/)); -test('calcSpa: NYC summer sunrise = 05:25:03', () => assert.equal(NYC_FMT.sunrise, '05:25:03')); -test('calcSpa: NYC summer noon = 12:57:56', () => assert.equal(NYC_FMT.solarNoon, '12:57:56')); -test('calcSpa: NYC summer sunset = 20:30:35', () => assert.equal(NYC_FMT.sunset, '20:30:35')); -test('calcSpa: zenith is number', () => assert.equal(typeof NYC_FMT.zenith, 'number')); - -// ─── Custom angles (twilight) ──────────────────────────────────────────────── - -const NYC_TWILIGHT = getSpa( - new Date('2025-06-21T00:00:00Z'), - 40.7128, -74.006, -4, - { elevation: 10 }, - [96, 102, 108], -); - -test('custom angles: result has angles array', () => assert.ok(Array.isArray(NYC_TWILIGHT.angles))); -test('custom angles: three entries', () => assert.equal(NYC_TWILIGHT.angles.length, 3)); -test('custom angles: civil twilight rise < standard rise', () => { - assert.ok(NYC_TWILIGHT.angles[0].sunrise < NYC_SUMMER.sunrise, 'civil rises before standard'); -}); -test('custom angles: nautical rise < civil rise', () => { - assert.ok(NYC_TWILIGHT.angles[1].sunrise < NYC_TWILIGHT.angles[0].sunrise); -}); -test('custom angles: astronomical rise < nautical rise', () => { - assert.ok(NYC_TWILIGHT.angles[2].sunrise < NYC_TWILIGHT.angles[1].sunrise); -}); - -const NYC_TWILIGHT_FMT = calcSpa( - new Date('2025-06-21T00:00:00Z'), - 40.7128, -74.006, -4, - { elevation: 10 }, - [96, 102, 108], -); - -test('calcSpa angles: formatted sunrise is string', () => { - assert.equal(typeof NYC_TWILIGHT_FMT.angles[0].sunrise, 'string'); -}); -test('calcSpa angles: HH:MM:SS format', () => { - assert.match(NYC_TWILIGHT_FMT.angles[0].sunrise, /^\d{2}:\d{2}:\d{2}$/); -}); - -// ─── Input validation ───────────────────────────────────────────────────────── - -test('validation: invalid Date throws TypeError', () => { - assert.throws(() => getSpa(new Date('invalid'), 40, -74, 0), TypeError); -}); -test('validation: non-number latitude throws TypeError', () => { - assert.throws(() => getSpa(new Date(), 'bad', -74, 0), TypeError); -}); -test('validation: latitude > 90 throws RangeError', () => { - assert.throws(() => getSpa(new Date(), 91, -74, 0), RangeError); -}); -test('validation: latitude < -90 throws RangeError', () => { - assert.throws(() => getSpa(new Date(), -91, -74, 0), RangeError); -}); -test('validation: longitude > 180 throws RangeError', () => { - assert.throws(() => getSpa(new Date(), 40, 181, 0), RangeError); -}); -test('validation: longitude < -180 throws RangeError', () => { - assert.throws(() => getSpa(new Date(), 40, -181, 0), RangeError); -}); - -// ─── Defaults ──────────────────────────────────────────────────────────────── - -test('defaults: tz=null uses 0', () => { - const r = getSpa(new Date('2025-06-21T00:00:00Z'), 40.7128, -74.006, null); - assert.equal(typeof r.zenith, 'number'); -}); -test('defaults: no options arg', () => { - const r = getSpa(new Date('2025-06-21T00:00:00Z'), 40.7128, -74.006); - assert.equal(typeof r.sunrise, 'number'); -}); -// ─── Cape Town (southern hemisphere, summer) ────────────────────────────────── - -const CAPE_TOWN = getSpa( - new Date('2025-12-21T00:00:00Z'), - -33.9249, 18.4241, 2, - { elevation: 25, pressure: 1013, temperature: 18 }, -); - -test('Cape Town summer: sunrise ~05:31:55', () => close(CAPE_TOWN.sunrise, 5.532, 0.001, 'sunrise')); -test('Cape Town summer: sunset ~19:57:01', () => close(CAPE_TOWN.sunset, 19.950, 0.001, 'sunset')); - -// ─── Reykjavik (midnight sun) ───────────────────────────────────────────────── - -const REYKJAVIK = getSpa( - new Date('2025-06-21T00:00:00Z'), - 64.1466, -21.9426, 0, - { elevation: 0, pressure: 1013, temperature: 10 }, -); - -test('Reykjavik midsummer: sunrise ~02:55', () => close(REYKJAVIK.sunrise, 2.919, 0.001, 'sunrise')); -// Sunset wraps past midnight, so the raw value > 24 or suntransit is reliable -test('Reykjavik midsummer: solarNoon in range', () => { - assert.ok(REYKJAVIK.solarNoon > 12 && REYKJAVIK.solarNoon < 15); -}); - -// ─── SPA_ZA function code (zenith/azimuth only, no RTS) ────────────────────── - -const ZA_ONLY = getSpa( - new Date('2025-06-21T00:00:00Z'), - 40.7128, -74.006, -4, - { function: SPA_ZA }, -); - -test('SPA_ZA: zenith is a finite number', () => assert.ok(isFinite(ZA_ONLY.zenith))); -test('SPA_ZA: azimuth is a finite number', () => assert.ok(isFinite(ZA_ONLY.azimuth))); -test('SPA_ZA: sunrise is NaN (not computed)', () => assert.ok(isNaN(ZA_ONLY.sunrise))); -test('SPA_ZA: solarNoon is NaN (not computed)', () => assert.ok(isNaN(ZA_ONLY.solarNoon))); -test('SPA_ZA: sunset is NaN (not computed)', () => assert.ok(isNaN(ZA_ONLY.sunset))); - -test('SPA_ZA calcSpa: sunrise is N/A', () => { - const r = calcSpa(new Date('2025-06-21T00:00:00Z'), 40.7128, -74.006, -4, { function: SPA_ZA }); - assert.equal(r.sunrise, 'N/A'); - assert.equal(r.solarNoon, 'N/A'); - assert.equal(r.sunset, 'N/A'); -}); - -// ─── Function code validation ───────────────────────────────────────────────── - -test('validation: invalid function code throws RangeError', () => { - assert.throws( - () => getSpa(new Date('2025-06-21T00:00:00Z'), 40.7128, -74.006, -4, { function: 99 }), - RangeError, +describe('getSpa: NYC summer solstice', () => { + const NYC_SUMMER = getSpa( + new Date('2025-06-21T00:00:00Z'), + 40.7128, -74.006, -4, + { elevation: 10, pressure: 1013, temperature: 20 }, ); -}); -test('validation: angles + SPA_ZA throws RangeError', () => { - assert.throws( - () => getSpa(new Date('2025-06-21T00:00:00Z'), 40.7128, -74.006, -4, { function: SPA_ZA }, [96, 102]), - RangeError, - ); -}); -test('validation: angles + SPA_ZA_INC throws RangeError', () => { - assert.throws( - () => getSpa(new Date('2025-06-21T00:00:00Z'), 40.7128, -74.006, -4, { function: SPA_ZA_INC }, [96]), - RangeError, - ); -}); -test('validation: empty angles array returns plain SpaResult', () => { - const r = getSpa(new Date('2025-06-21T00:00:00Z'), 40.7128, -74.006, -4, {}, []); - assert.ok(!('angles' in r)); -}); -test('validation: calcSpa with empty angles does not crash', () => { - const r = calcSpa(new Date('2025-06-21T00:00:00Z'), 40.7128, -74.006, -4, {}, []); - assert.equal(typeof r.sunrise, 'string'); - assert.ok(!('angles' in r)); + + it('sunrise ~05:25:03', () => close(NYC_SUMMER.sunrise, 5.417, 0.001, 'sunrise')); + it('solarNoon ~12:57:56', () => close(NYC_SUMMER.solarNoon, 12.965, 0.001, 'solarNoon')); + it('sunset ~20:30:35', () => close(NYC_SUMMER.sunset, 20.509, 0.001, 'sunset')); + it('zenith is a number', () => assert.equal(typeof NYC_SUMMER.zenith, 'number')); + it('azimuth 0-360', () => { + assert.ok(NYC_SUMMER.azimuth >= 0 && NYC_SUMMER.azimuth <= 360, `azimuth ${NYC_SUMMER.azimuth}`); + }); }); -// ─── Summary ───────────────────────────────────────────────────────────────── +describe('getSpa: NYC winter solstice', () => { + const NYC_WINTER = getSpa( + new Date('2025-12-21T00:00:00Z'), + 40.7128, -74.006, -5, + { elevation: 10, pressure: 1013, temperature: 5 }, + ); -console.log('---'); -console.log(`${passed + failed} tests total: ${passed} passed, ${failed} failed`); -if (failed > 0) process.exit(1); + it('sunrise ~07:16:41', () => close(NYC_WINTER.sunrise, 7.278, 0.001, 'sunrise')); + it('solarNoon ~11:54:19', () => close(NYC_WINTER.solarNoon, 11.905, 0.001, 'solarNoon')); + it('sunset ~16:31:56', () => close(NYC_WINTER.sunset, 16.532, 0.001, 'sunset')); +}); + +describe('getSpa: London summer', () => { + const LONDON_SUMMER = getSpa( + new Date('2025-06-21T00:00:00Z'), + 51.5074, -0.1278, 1, + { elevation: 11, pressure: 1013, temperature: 18 }, + ); + + it('sunrise ~04:43:07', () => close(LONDON_SUMMER.sunrise, 4.718, 0.001, 'sunrise')); + it('sunset ~21:21:37', () => close(LONDON_SUMMER.sunset, 21.360, 0.001, 'sunset')); +}); + +describe('getSpa: Tokyo', () => { + const TOKYO_SUMMER = getSpa( + new Date('2025-06-21T00:00:00Z'), + 35.6895, 139.6917, 9, + { elevation: 40, pressure: 1013, temperature: 22 }, + ); + + it('sunrise ~04:25:52', () => close(TOKYO_SUMMER.sunrise, 4.431, 0.001, 'sunrise')); + it('sunset ~19:00:22', () => close(TOKYO_SUMMER.sunset, 19.006, 0.001, 'sunset')); +}); + +describe('getSpa: Sydney winter (southern hemisphere)', () => { + const SYDNEY_WINTER = getSpa( + new Date('2025-06-21T00:00:00Z'), + -33.8688, 151.2093, 10, + { elevation: 58, pressure: 1013, temperature: 15 }, + ); + + it('sunrise ~07:00:12', () => close(SYDNEY_WINTER.sunrise, 7.003, 0.001, 'sunrise')); + it('sunset ~16:53:52', () => close(SYDNEY_WINTER.sunset, 16.898, 0.001, 'sunset')); +}); + +describe('getSpa: Quito (equator, equinox)', () => { + const QUITO_EQUINOX = getSpa( + new Date('2025-03-20T00:00:00Z'), + -0.1807, -78.4678, -5, + { elevation: 2850, pressure: 789, temperature: 14 }, + ); + + it('sunrise ~06:17:54', () => close(QUITO_EQUINOX.sunrise, 6.298, 0.001, 'sunrise')); + it('sunset ~18:24:25', () => close(QUITO_EQUINOX.sunset, 18.407, 0.001, 'sunset')); +}); + +describe('getSpa: polar night (Tromso, arctic winter)', () => { + const TROMSO_POLAR = getSpa( + new Date('2025-12-21T00:00:00Z'), + 69.6492, 18.9553, 1, + { elevation: 0, pressure: 1013, temperature: -2 }, + ); + + it('sunrise < 0 (polar night sentinel)', () => assert.ok(TROMSO_POLAR.sunrise < 0)); + it('zenith > 90 (sun below horizon)', () => assert.ok(TROMSO_POLAR.zenith > 90)); +}); + +describe('calcSpa: formatted output', () => { + const NYC_FMT = calcSpa( + new Date('2025-06-21T00:00:00Z'), + 40.7128, -74.006, -4, + { elevation: 10, pressure: 1013, temperature: 20 }, + ); + + it('sunrise is string', () => assert.equal(typeof NYC_FMT.sunrise, 'string')); + it('solarNoon is string', () => assert.equal(typeof NYC_FMT.solarNoon, 'string')); + it('sunset is string', () => assert.equal(typeof NYC_FMT.sunset, 'string')); + it('sunrise format HH:MM:SS', () => assert.match(NYC_FMT.sunrise, /^\d{2}:\d{2}:\d{2}$/)); + it('NYC summer sunrise = 05:25:03', () => assert.equal(NYC_FMT.sunrise, '05:25:03')); + it('NYC summer noon = 12:57:56', () => assert.equal(NYC_FMT.solarNoon, '12:57:56')); + it('NYC summer sunset = 20:30:35', () => assert.equal(NYC_FMT.sunset, '20:30:35')); + it('zenith is number', () => assert.equal(typeof NYC_FMT.zenith, 'number')); +}); + +describe('custom angles (twilight)', () => { + const NYC_SUMMER = getSpa( + new Date('2025-06-21T00:00:00Z'), + 40.7128, -74.006, -4, + { elevation: 10, pressure: 1013, temperature: 20 }, + ); + + const NYC_TWILIGHT = getSpa( + new Date('2025-06-21T00:00:00Z'), + 40.7128, -74.006, -4, + { elevation: 10 }, + [96, 102, 108], + ); + + it('result has angles array', () => assert.ok(Array.isArray(NYC_TWILIGHT.angles))); + it('three entries', () => assert.equal(NYC_TWILIGHT.angles.length, 3)); + it('civil twilight rise < standard rise', () => { + assert.ok(NYC_TWILIGHT.angles[0].sunrise < NYC_SUMMER.sunrise, 'civil rises before standard'); + }); + it('nautical rise < civil rise', () => { + assert.ok(NYC_TWILIGHT.angles[1].sunrise < NYC_TWILIGHT.angles[0].sunrise); + }); + it('astronomical rise < nautical rise', () => { + assert.ok(NYC_TWILIGHT.angles[2].sunrise < NYC_TWILIGHT.angles[1].sunrise); + }); + + const NYC_TWILIGHT_FMT = calcSpa( + new Date('2025-06-21T00:00:00Z'), + 40.7128, -74.006, -4, + { elevation: 10 }, + [96, 102, 108], + ); + + it('calcSpa angles: formatted sunrise is string', () => { + assert.equal(typeof NYC_TWILIGHT_FMT.angles[0].sunrise, 'string'); + }); + it('calcSpa angles: HH:MM:SS format', () => { + assert.match(NYC_TWILIGHT_FMT.angles[0].sunrise, /^\d{2}:\d{2}:\d{2}$/); + }); +}); + +describe('input validation', () => { + it('invalid Date throws TypeError', () => { + assert.throws(() => getSpa(new Date('invalid'), 40, -74, 0), TypeError); + }); + it('non-number latitude throws TypeError', () => { + assert.throws(() => getSpa(new Date(), 'bad', -74, 0), TypeError); + }); + it('latitude > 90 throws RangeError', () => { + assert.throws(() => getSpa(new Date(), 91, -74, 0), RangeError); + }); + it('latitude < -90 throws RangeError', () => { + assert.throws(() => getSpa(new Date(), -91, -74, 0), RangeError); + }); + it('longitude > 180 throws RangeError', () => { + assert.throws(() => getSpa(new Date(), 40, 181, 0), RangeError); + }); + it('longitude < -180 throws RangeError', () => { + assert.throws(() => getSpa(new Date(), 40, -181, 0), RangeError); + }); + it('NaN latitude throws TypeError', () => { + assert.throws(() => getSpa(new Date(), NaN, -74, 0), TypeError); + }); + it('Infinity latitude throws TypeError', () => { + assert.throws(() => getSpa(new Date(), Infinity, -74, 0), TypeError); + }); + it('NaN longitude throws TypeError', () => { + assert.throws(() => getSpa(new Date(), 40, NaN, 0), TypeError); + }); + it('Infinity longitude throws TypeError', () => { + assert.throws(() => getSpa(new Date(), 40, Infinity, 0), TypeError); + }); + it('timezone > 18 throws RangeError', () => { + assert.throws(() => getSpa(new Date(), 40, -74, 19), RangeError); + }); + it('timezone < -18 throws RangeError', () => { + assert.throws(() => getSpa(new Date(), 40, -74, -19), RangeError); + }); + it('NaN timezone throws TypeError', () => { + assert.throws(() => getSpa(new Date(), 40, -74, NaN), TypeError); + }); + it('non-finite option field throws TypeError', () => { + assert.throws(() => getSpa(new Date(), 40, -74, 0, { elevation: Infinity }), TypeError); + }); + it('NaN option field throws TypeError', () => { + assert.throws(() => getSpa(new Date(), 40, -74, 0, { pressure: NaN }), TypeError); + }); + it('angle out of range throws RangeError', () => { + assert.throws(() => getSpa(new Date(), 40, -74, 0, null, [200]), RangeError); + }); + it('negative angle throws RangeError', () => { + assert.throws(() => getSpa(new Date(), 40, -74, 0, null, [-1]), RangeError); + }); + it('NaN angle throws TypeError', () => { + assert.throws(() => getSpa(new Date(), 40, -74, 0, null, [NaN]), TypeError); + }); +}); + +describe('defaults', () => { + it('tz=null uses 0', () => { + const r = getSpa(new Date('2025-06-21T00:00:00Z'), 40.7128, -74.006, null); + assert.equal(typeof r.zenith, 'number'); + }); + it('no options arg', () => { + const r = getSpa(new Date('2025-06-21T00:00:00Z'), 40.7128, -74.006); + assert.equal(typeof r.sunrise, 'number'); + }); +}); + +describe('Cape Town (southern hemisphere, summer)', () => { + const CAPE_TOWN = getSpa( + new Date('2025-12-21T00:00:00Z'), + -33.9249, 18.4241, 2, + { elevation: 25, pressure: 1013, temperature: 18 }, + ); + + it('sunrise ~05:31:55', () => close(CAPE_TOWN.sunrise, 5.532, 0.001, 'sunrise')); + it('sunset ~19:57:01', () => close(CAPE_TOWN.sunset, 19.950, 0.001, 'sunset')); +}); + +describe('Reykjavik (midnight sun)', () => { + const REYKJAVIK = getSpa( + new Date('2025-06-21T00:00:00Z'), + 64.1466, -21.9426, 0, + { elevation: 0, pressure: 1013, temperature: 10 }, + ); + + it('sunrise ~02:55', () => close(REYKJAVIK.sunrise, 2.919, 0.001, 'sunrise')); + it('solarNoon in range', () => { + assert.ok(REYKJAVIK.solarNoon > 12 && REYKJAVIK.solarNoon < 15); + }); +}); + +describe('SPA_ZA function code (zenith/azimuth only)', () => { + const ZA_ONLY = getSpa( + new Date('2025-06-21T00:00:00Z'), + 40.7128, -74.006, -4, + { function: SPA_ZA }, + ); + + it('zenith is a finite number', () => assert.ok(isFinite(ZA_ONLY.zenith))); + it('azimuth is a finite number', () => assert.ok(isFinite(ZA_ONLY.azimuth))); + it('sunrise is NaN (not computed)', () => assert.ok(isNaN(ZA_ONLY.sunrise))); + it('solarNoon is NaN (not computed)', () => assert.ok(isNaN(ZA_ONLY.solarNoon))); + it('sunset is NaN (not computed)', () => assert.ok(isNaN(ZA_ONLY.sunset))); + + it('calcSpa: sunrise is N/A', () => { + const r = calcSpa(new Date('2025-06-21T00:00:00Z'), 40.7128, -74.006, -4, { function: SPA_ZA }); + assert.equal(r.sunrise, 'N/A'); + assert.equal(r.solarNoon, 'N/A'); + assert.equal(r.sunset, 'N/A'); + }); +}); + +describe('function code validation', () => { + it('invalid function code throws RangeError', () => { + assert.throws( + () => getSpa(new Date('2025-06-21T00:00:00Z'), 40.7128, -74.006, -4, { function: 99 }), + RangeError, + ); + }); + it('angles + SPA_ZA throws RangeError', () => { + assert.throws( + () => getSpa(new Date('2025-06-21T00:00:00Z'), 40.7128, -74.006, -4, { function: SPA_ZA }, [96, 102]), + RangeError, + ); + }); + it('angles + SPA_ZA_INC throws RangeError', () => { + assert.throws( + () => getSpa(new Date('2025-06-21T00:00:00Z'), 40.7128, -74.006, -4, { function: SPA_ZA_INC }, [96]), + RangeError, + ); + }); + it('empty angles array returns plain SpaResult', () => { + const r = getSpa(new Date('2025-06-21T00:00:00Z'), 40.7128, -74.006, -4, {}, []); + assert.ok(!('angles' in r)); + }); + it('calcSpa with empty angles does not crash', () => { + const r = calcSpa(new Date('2025-06-21T00:00:00Z'), 40.7128, -74.006, -4, {}, []); + assert.equal(typeof r.sunrise, 'string'); + assert.ok(!('angles' in r)); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 6720bc6..27004ac 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,8 @@ "moduleResolution": "bundler", "lib": ["ES2020"], "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true,