diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c4d8bff --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[*.{json,yml,yaml}] +indent_size = 2 diff --git a/.github/actions/report/.prettierignore b/.github/actions/report/.prettierignore new file mode 100644 index 0000000..e848f85 --- /dev/null +++ b/.github/actions/report/.prettierignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +coverage/ +package-lock.json diff --git a/.github/actions/report/.prettierrc.json b/.github/actions/report/.prettierrc.json new file mode 100644 index 0000000..4cbc711 --- /dev/null +++ b/.github/actions/report/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "semi": true, + "singleQuote": true, + "trailingComma": "all", + "printWidth": 100, + "tabWidth": 2 +} diff --git a/.github/actions/report/README.md b/.github/actions/report/README.md index 6a8b258..18c1148 100644 --- a/.github/actions/report/README.md +++ b/.github/actions/report/README.md @@ -13,12 +13,12 @@ configured threshold is breached. ```yaml permissions: - id-token: write # mint OIDC token for Worker auth - checks: write # post Check Run on PRs + id-token: write # mint OIDC token for Worker auth + checks: write # post Check Run on PRs steps: - uses: actions/checkout@v4 - - run: npm test -- --coverage # your tests write coverage/lcov.info + - run: npm test -- --coverage # your tests write coverage/lcov.info - uses: CoverageTracker/coverage-tracker/.github/actions/report@v0.4.0 with: worker-url: https://coverage-tracker.yourdomain.com @@ -26,27 +26,27 @@ steps: ## Inputs -| Input | Required | Default | Description | -|---|---|---|---| -| `worker-url` | **yes** | — | Base URL of your coverage-tracker Worker. | -| `coverage-path` | no | *(probed)* | Path to the coverage report. When unset, the documented per-language default paths are probed in a fixed order; the first hit wins. **Missing coverage is always a hard failure** (the error lists every probed path). An explicit path always wins. | -| `coverage-tool` | no | `''` | Only relevant for Cobertura, whose branch semantics vary by producer (`gocover-cobertura`, `kcov`, `covertool`, `phpunit`, `gcovr`). Naming it silences the "trusting the data" warning. Ignored for LCOV / JaCoCo / Go. | -| `complexity-path` | no | *(probed)* | Radon JSON, gocyclo text, or Lizard XML. When unset, probes `radon.json` → `gocyclo.txt` → `lizard-report.xml`. | -| `duplication-path` | no | *(probed)* | jscpd JSON. When unset, probes `jscpd-report/jscpd-report.json`. The Action no longer runs jscpd — run it yourself. | -| `min-coverage` | no | `''` | Absolute coverage floor (%). | -| `max-coverage-drop` | no | `''` | Max allowed drop (pp) from the default-branch baseline (PR checks only). | -| `max-complexity` | no | `''` | Max allowed average cyclomatic complexity. | -| `max-duplication` | no | `''` | Max allowed duplication (%). | -| `github-token` | no | `${{ github.token }}` | Token used to post the PR Check Run. | +| Input | Required | Default | Description | +| ------------------- | -------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `worker-url` | **yes** | — | Base URL of your coverage-tracker Worker. | +| `coverage-path` | no | _(probed)_ | Path to the coverage report. When unset, the documented per-language default paths are probed in a fixed order; the first hit wins. **Missing coverage is always a hard failure** (the error lists every probed path). An explicit path always wins. | +| `coverage-tool` | no | `''` | Only relevant for Cobertura, whose branch semantics vary by producer (`gocover-cobertura`, `kcov`, `covertool`, `phpunit`, `gcovr`). Naming it silences the "trusting the data" warning. Ignored for LCOV / JaCoCo / Go. | +| `complexity-path` | no | _(probed)_ | Radon JSON, gocyclo text, or Lizard XML. When unset, probes `radon.json` → `gocyclo.txt` → `lizard-report.xml`. | +| `duplication-path` | no | _(probed)_ | jscpd JSON. When unset, probes `jscpd-report/jscpd-report.json`. The Action no longer runs jscpd — run it yourself. | +| `min-coverage` | no | `''` | Absolute coverage floor (%). | +| `max-coverage-drop` | no | `''` | Max allowed drop (pp) from the default-branch baseline (PR checks only). | +| `max-complexity` | no | `''` | Max allowed average cyclomatic complexity. | +| `max-duplication` | no | `''` | Max allowed duplication (%). | +| `github-token` | no | `${{ github.token }}` | Token used to post the PR Check Run. | ## Coverage formats (auto-detected by content) -| Signal | Format | Parsed as | -|---|---|---| -| First line `mode: set\|count\|atomic` | Go coverage profile | statement coverage | -| Starts with `TN:` / `SF:` | LCOV | line + branch (if present) | -| XML root `` | Cobertura | line + branch (per quirks) | -| XML root `` | JaCoCo | line + branch + cyclomatic (free) | +| Signal | Format | Parsed as | +| ------------------------------------- | ------------------- | --------------------------------- | +| First line `mode: set\|count\|atomic` | Go coverage profile | statement coverage | +| Starts with `TN:` / `SF:` | LCOV | line + branch (if present) | +| XML root `` | Cobertura | line + branch (per quirks) | +| XML root `` | JaCoCo | line + branch + cyclomatic (free) | For per-language commands and the full default-path table, see [docs/generating-coverage-reports.md](../../../docs/generating-coverage-reports.md). @@ -58,7 +58,7 @@ configured for them**: - **Coverage** is always required. If no report is found (explicit path or probe), the Action fails and lists every path it probed. -- **Complexity / duplication** are skipped silently when the metric is *both* +- **Complexity / duplication** are skipped silently when the metric is _both_ unconfigured and absent. But if `max-complexity` / `max-duplication` is set and no matching report is found, the Action **fails** with an actionable error — a configured threshold with no data is treated as a mistake, not a diff --git a/.github/actions/report/dist/index.js b/.github/actions/report/dist/index.js index 7067dd0..1b40db1 100644 --- a/.github/actions/report/dist/index.js +++ b/.github/actions/report/dist/index.js @@ -24779,7 +24779,9 @@ async function runPRCheck(workerUrl, oidcToken, metrics, owner, repo, category = const data = await res.json(); baselines[m.name] = data.value; } catch { - warning(`Baseline fetch for "${m.name}" returned non-JSON body (HTTP ${res.status}) \u2014 skipping baseline.`); + warning( + `Baseline fetch for "${m.name}" returned non-JSON body (HTTP ${res.status}) \u2014 skipping baseline.` + ); } } else if (res.status !== 404) { warning(`Baseline fetch for "${m.name}" returned HTTP ${res.status}.`); @@ -24811,7 +24813,13 @@ async function runPRCheck(workerUrl, oidcToken, metrics, owner, repo, category = } const failed = reasons.length > 0; if (failed) anyFailed = true; - const hasThreshold = thresholdConfigured(m.name, minCoverage, maxCoverageDrop, maxComplexity, maxDuplication); + const hasThreshold = thresholdConfigured( + m.name, + minCoverage, + maxCoverageDrop, + maxComplexity, + maxDuplication + ); results.push({ metric: m.name, current: m.value, @@ -30200,7 +30208,7 @@ function parseLizard(content) { // src/complexity/detect.ts function detectComplexityShape(content) { - const trimmed = content.replace(/^/, "").trimStart(); + const trimmed = content.replace(/^\uFEFF/, "").trimStart(); if (trimmed.startsWith("{") || trimmed.startsWith("[")) return "radon"; if (trimmed.startsWith("<")) return "lizard"; return "gocyclo"; diff --git a/.github/actions/report/eslint.config.mjs b/.github/actions/report/eslint.config.mjs new file mode 100644 index 0000000..86cb1e4 --- /dev/null +++ b/.github/actions/report/eslint.config.mjs @@ -0,0 +1,12 @@ +import js from '@eslint/js'; +import tseslint from 'typescript-eslint'; +import eslintConfigPrettier from 'eslint-config-prettier'; +import globals from 'globals'; + +export default tseslint.config( + { ignores: ['dist/**', 'coverage/**', 'node_modules/**'] }, + js.configs.recommended, + ...tseslint.configs.recommended, + { languageOptions: { globals: { ...globals.node } } }, + eslintConfigPrettier, +); diff --git a/.github/actions/report/package-lock.json b/.github/actions/report/package-lock.json index 9ffb038..bfab21b 100644 --- a/.github/actions/report/package-lock.json +++ b/.github/actions/report/package-lock.json @@ -14,10 +14,16 @@ "fast-xml-parser": "^5.9.3" }, "devDependencies": { + "@eslint/js": "^9.0.0", "@types/node": "^22.0.0", "@vitest/coverage-v8": "^3.0.0", "esbuild": "^0.25.0", + "eslint": "^9.0.0", + "eslint-config-prettier": "^9.1.0", + "globals": "^15.9.0", + "prettier": "^3.3.0", "typescript": "^5.8.0", + "typescript-eslint": "^8.0.0", "vitest": "^3.0.0" } }, @@ -597,6 +603,291 @@ "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.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", + "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.0", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.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/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1229,6 +1520,13 @@ "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/@types/node": { "version": "22.20.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.0.tgz", @@ -1239,6 +1537,249 @@ "undici-types": "~6.21.0" } }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.63.0.tgz", + "integrity": "sha512-rvwSgqT+DHpWdzfSzPatRLm02a0GlESt++9iy3hLCDY4BgkaLcl8LBi9Yh7XGFBpwcBE/K3024QuXWTpbz4FfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.63.0", + "@typescript-eslint/type-utils": "8.63.0", + "@typescript-eslint/utils": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.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.63.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.63.0.tgz", + "integrity": "sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.63.0", + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.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.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.63.0.tgz", + "integrity": "sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.63.0", + "@typescript-eslint/types": "^8.63.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.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.63.0.tgz", + "integrity": "sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.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.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.63.0.tgz", + "integrity": "sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==", + "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.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.63.0.tgz", + "integrity": "sha512-Nzzh/OGxVCOjObjaj1CQF2RUasyYy2Jfuh+zZ3PjLzG2fYRriAiZLib9UKtO+CpQAS3YHiAS+ckZDclwqI1TPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.0", + "@typescript-eslint/utils": "8.63.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.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.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.63.0.tgz", + "integrity": "sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==", + "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.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.63.0.tgz", + "integrity": "sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.63.0", + "@typescript-eslint/tsconfig-utils": "8.63.0", + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.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.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.63.0.tgz", + "integrity": "sha512-fUKaeAvrTuQg/Tgt3nliAUSZHJM6DlCcfyEmxCvlX8kieWSStBX+5O5Fnidtc3i2JrH+9c/GL4RY2iasd/GPTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.63.0", + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.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.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.63.0.tgz", + "integrity": "sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.63.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/@typescript-eslint/visitor-keys/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/@vitest/coverage-v8": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.6.tgz", @@ -1388,6 +1929,46 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "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.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "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/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", @@ -1426,6 +2007,13 @@ ], "license": "MIT" }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -1487,6 +2075,16 @@ "node": ">=8" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/chai": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", @@ -1504,6 +2102,39 @@ "node": ">=18" } }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/check-error": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", @@ -1534,6 +2165,13 @@ "dev": true, "license": "MIT" }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, "node_modules/content-type": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", @@ -1590,6 +2228,13 @@ "node": ">=6" } }, + "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/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -1653,6 +2298,207 @@ "@esbuild/win32-x64": "0.25.12" } }, + "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": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.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", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.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", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", + "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "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", @@ -1663,6 +2509,16 @@ "@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.4.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", @@ -1673,6 +2529,27 @@ "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/fast-xml-builder": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", @@ -1718,18 +2595,69 @@ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" + "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/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" }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "engines": { + "node": ">=16" } }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -1784,6 +2712,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "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/glob/node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1817,6 +2758,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "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", @@ -1834,6 +2788,53 @@ "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/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -1844,6 +2845,19 @@ "node": ">=8" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-unsafe": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-1.0.1.tgz", @@ -1940,12 +2954,103 @@ "dev": true, "license": "MIT" }, + "node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "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/json-with-bigint": { "version": "3.5.8", "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.8.tgz", "integrity": "sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw==", "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/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/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/loupe": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", @@ -2050,6 +3155,63 @@ "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/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/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -2057,6 +3219,29 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "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-expression-matcher": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.1.tgz", @@ -2165,6 +3350,52 @@ "node": "^10 || ^12 || >=14" } }, + "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/prettier": { + "version": "3.9.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.5.tgz", + "integrity": "sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "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/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/rollup": { "version": "4.62.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", @@ -2394,6 +3625,19 @@ "node": ">=8" } }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-literal": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", @@ -2518,6 +3762,19 @@ "node": ">=14.0.0" } }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/tunnel": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", @@ -2527,6 +3784,19 @@ "node": ">=0.6.11 <=0.7.0 || >=0.7.3" } }, + "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", @@ -2541,6 +3811,30 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.63.0.tgz", + "integrity": "sha512-xgwXyzG4sK9ALkBxbyGkTMMOS+imnW65iPhxCQMK83KhxyoDNW7l+IDqEf9vMdoUidHpOoS967RCq4eMiTexwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.63.0", + "@typescript-eslint/parser": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.0", + "@typescript-eslint/utils": "8.63.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.1.0" + } + }, "node_modules/undici": { "version": "6.27.0", "resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz", @@ -2563,6 +3857,16 @@ "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", "license": "ISC" }, + "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.6", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", @@ -3251,6 +4555,16 @@ "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/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -3363,6 +4677,19 @@ "engines": { "node": ">=16.0.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/.github/actions/report/package.json b/.github/actions/report/package.json index 20400cc..57f19c1 100644 --- a/.github/actions/report/package.json +++ b/.github/actions/report/package.json @@ -7,6 +7,10 @@ "scripts": { "build": "esbuild src/index.ts --bundle --platform=node --target=node24 --outfile=dist/index.js", "typecheck": "tsc --noEmit", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "format": "prettier --write .", + "format:check": "prettier --check .", "test": "vitest run --coverage" }, "dependencies": { @@ -16,10 +20,16 @@ "fast-xml-parser": "^5.9.3" }, "devDependencies": { + "@eslint/js": "^9.0.0", "@types/node": "^22.0.0", "@vitest/coverage-v8": "^3.0.0", "esbuild": "^0.25.0", + "eslint": "^9.0.0", + "eslint-config-prettier": "^9.1.0", + "globals": "^15.9.0", + "prettier": "^3.3.0", "typescript": "^5.8.0", + "typescript-eslint": "^8.0.0", "vitest": "^3.0.0" } } diff --git a/.github/actions/report/src/__tests__/complexity.test.ts b/.github/actions/report/src/__tests__/complexity.test.ts index 2d1dba1..c5bb935 100644 --- a/.github/actions/report/src/__tests__/complexity.test.ts +++ b/.github/actions/report/src/__tests__/complexity.test.ts @@ -68,7 +68,7 @@ describe('parseLizard', () => { expect(parseLizard(xml)).toEqual({ cyclomatic: 4 }); }); - it('uses the final cumulative average across a multi-file report, not the first file\'s', () => { + it("uses the final cumulative average across a multi-file report, not the first file's", () => { // Real lizard --xml output for a multi-file project writes a running // cumulative after each file's items (all as flat // siblings, since the format has no per-file nesting) — only the last @@ -100,6 +100,8 @@ describe('detectComplexityShape', () => { expect(parseComplexity('{"a.py":[{"type":"function","complexity":2}]}')).toEqual({ cyclomatic: 2, }); - expect(parseComplexity('2 pkg fn file.go:1:1\n4 pkg gn file.go:2:1')).toEqual({ cyclomatic: 3 }); + expect(parseComplexity('2 pkg fn file.go:1:1\n4 pkg gn file.go:2:1')).toEqual({ + cyclomatic: 3, + }); }); }); diff --git a/.github/actions/report/src/__tests__/format.test.ts b/.github/actions/report/src/__tests__/format.test.ts index d94af40..566c4a1 100644 --- a/.github/actions/report/src/__tests__/format.test.ts +++ b/.github/actions/report/src/__tests__/format.test.ts @@ -14,9 +14,9 @@ describe('sniffCoverageFormat', () => { }); it('detects Cobertura from the root', () => { - expect(sniffCoverageFormat('\n')).toBe( - 'cobertura', - ); + expect( + sniffCoverageFormat('\n'), + ).toBe('cobertura'); }); it('detects JaCoCo from the root', () => { diff --git a/.github/actions/report/src/__tests__/jacoco.test.ts b/.github/actions/report/src/__tests__/jacoco.test.ts index ae51b2b..390e141 100644 --- a/.github/actions/report/src/__tests__/jacoco.test.ts +++ b/.github/actions/report/src/__tests__/jacoco.test.ts @@ -1,8 +1,7 @@ import { describe, it, expect } from 'vitest'; import { parseJacoco } from '../jacoco'; -const report = (counters: string) => - `\n${counters}`; +const report = (counters: string) => `\n${counters}`; describe('parseJacoco', () => { it('computes line and branch coverage from report-level counters', () => { diff --git a/.github/actions/report/src/__tests__/run.test.ts b/.github/actions/report/src/__tests__/run.test.ts index 73df01b..27cd0c7 100644 --- a/.github/actions/report/src/__tests__/run.test.ts +++ b/.github/actions/report/src/__tests__/run.test.ts @@ -1,9 +1,22 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import * as core from '@actions/core'; import * as fs from 'fs'; -import { run, runIngest, runPRCheck, postCheckRun, type Metric, type ThresholdResult } from '../run.js'; +import { + run, + runIngest, + runPRCheck, + postCheckRun, + type Metric, + type ThresholdResult, +} from '../run.js'; // Pure helper tests still live here too -import { parseThreshold, thresholdConfigured, formatValue, formatDelta, buildSummary } from '../run.js'; +import { + parseThreshold, + thresholdConfigured, + formatValue, + formatDelta, + buildSummary, +} from '../run.js'; // ── Hoisted mutable state for module mocks ──────────────────────────────────── // vi.hoisted runs during the hoisting phase so these variables are available @@ -101,8 +114,7 @@ describe('thresholdConfigured', () => { describe('formatValue', () => { it('formats a percentage to one decimal place', () => expect(formatValue(82.4, '%')).toBe('82.4%')); - it('formats a score to two decimal places', () => - expect(formatValue(4.2, 'score')).toBe('4.20')); + it('formats a score to two decimal places', () => expect(formatValue(4.2, 'score')).toBe('4.20')); it('formats 100% correctly', () => expect(formatValue(100, '%')).toBe('100.0%')); it('formats an integer score to two decimal places', () => expect(formatValue(5, 'score')).toBe('5.00')); @@ -122,14 +134,28 @@ describe('formatDelta', () => { describe('buildSummary', () => { const pass: ThresholdResult = { - metric: 'coverage', current: 85, baseline: 80, unit: '%', status: 'pass', reason: '', + metric: 'coverage', + current: 85, + baseline: 80, + unit: '%', + status: 'pass', + reason: '', }; const fail: ThresholdResult = { - metric: 'complexity', current: 12, baseline: 8, unit: 'score', status: 'fail', + metric: 'complexity', + current: 12, + baseline: 8, + unit: 'score', + status: 'fail', reason: 'exceeds max-complexity of 10', }; const info: ThresholdResult = { - metric: 'duplication', current: 2.5, baseline: null, unit: '%', status: 'info', reason: '', + metric: 'duplication', + current: 2.5, + baseline: null, + unit: '%', + status: 'info', + reason: '', }; it('contains the markdown table header', () => { @@ -159,7 +185,9 @@ describe('run()', () => { beforeEach(() => { vi.mocked(core.getIDToken).mockResolvedValue('mock-oidc-token'); vi.mocked(fs.existsSync).mockReturnValue(true); - vi.mocked(fs.readFileSync).mockReturnValue(METRICS_ONE as unknown as ReturnType); + vi.mocked(fs.readFileSync).mockReturnValue( + METRICS_ONE as unknown as ReturnType, + ); mockPayload.repository = { default_branch: 'main' }; mockChecksCreate.mockResolvedValue({}); mockFetch.mockResolvedValue(okFetchResponse({ ok: true, inserted: 1 })); @@ -195,11 +223,11 @@ describe('run()', () => { }); it('warns and returns early when metrics array is empty', async () => { - vi.mocked(fs.readFileSync).mockReturnValue(METRICS_EMPTY as unknown as ReturnType); - await run(); - expect(vi.mocked(core.warning)).toHaveBeenCalledWith( - 'No metrics collected — skipping report.', + vi.mocked(fs.readFileSync).mockReturnValue( + METRICS_EMPTY as unknown as ReturnType, ); + await run(); + expect(vi.mocked(core.warning)).toHaveBeenCalledWith('No metrics collected — skipping report.'); expect(mockFetch).not.toHaveBeenCalled(); }); @@ -260,9 +288,7 @@ describe('runIngest()', () => { it('calls core.setFailed on a non-OK HTTP response', async () => { mockFetch.mockResolvedValue(errFetchResponse(422, 'Unprocessable entity')); await runIngest('https://worker.example.com', 'mock-token', metrics); - expect(vi.mocked(core.setFailed)).toHaveBeenCalledWith( - expect.stringContaining('HTTP 422'), - ); + expect(vi.mocked(core.setFailed)).toHaveBeenCalledWith(expect.stringContaining('HTTP 422')); }); it('sends typed coverage fields (not a metrics array) in the request body', async () => { @@ -353,18 +379,14 @@ describe('runPRCheck()', () => { it('fails when coverage drop exceeds max-coverage-drop', async () => { vi.stubEnv('MAX_COVERAGE_DROP', '3'); // baseline=80, current=75, drop=5 > 3 - mockFetch.mockResolvedValue( - okFetchResponse({ value: 80, unit: '%', commit_sha: 'abc' }), - ); + mockFetch.mockResolvedValue(okFetchResponse({ value: 80, unit: '%', commit_sha: 'abc' })); await runPRCheck(WORKER, TOKEN, coverageMetric, 'owner', 'repo'); expect(vi.mocked(core.setFailed)).toHaveBeenCalled(); }); it('passes when coverage drop is within max-coverage-drop', async () => { vi.stubEnv('MAX_COVERAGE_DROP', '10'); // baseline=80, current=75, drop=5 <= 10 - mockFetch.mockResolvedValue( - okFetchResponse({ value: 80, unit: '%', commit_sha: 'abc' }), - ); + mockFetch.mockResolvedValue(okFetchResponse({ value: 80, unit: '%', commit_sha: 'abc' })); await runPRCheck(WORKER, TOKEN, coverageMetric, 'owner', 'repo'); expect(vi.mocked(core.setFailed)).not.toHaveBeenCalled(); }); @@ -372,14 +394,26 @@ describe('runPRCheck()', () => { it('fails when complexity exceeds max-complexity', async () => { vi.stubEnv('MAX_COMPLEXITY', '10'); // current=15 > 10 mockFetch.mockResolvedValue(errFetchResponse(404)); - await runPRCheck(WORKER, TOKEN, [{ name: 'complexity', value: 15, unit: 'score' }], 'owner', 'repo'); + await runPRCheck( + WORKER, + TOKEN, + [{ name: 'complexity', value: 15, unit: 'score' }], + 'owner', + 'repo', + ); expect(vi.mocked(core.setFailed)).toHaveBeenCalled(); }); it('fails when duplication exceeds max-duplication', async () => { vi.stubEnv('MAX_DUPLICATION', '5'); // current=8 > 5 mockFetch.mockResolvedValue(errFetchResponse(404)); - await runPRCheck(WORKER, TOKEN, [{ name: 'duplication', value: 8, unit: '%' }], 'owner', 'repo'); + await runPRCheck( + WORKER, + TOKEN, + [{ name: 'duplication', value: 8, unit: '%' }], + 'owner', + 'repo', + ); expect(vi.mocked(core.setFailed)).toHaveBeenCalled(); }); diff --git a/.github/actions/report/src/complexity/detect.ts b/.github/actions/report/src/complexity/detect.ts index 5afc8e4..df690d4 100644 --- a/.github/actions/report/src/complexity/detect.ts +++ b/.github/actions/report/src/complexity/detect.ts @@ -10,7 +10,7 @@ import { parseLizard } from './lizard'; export type ComplexityShape = 'radon' | 'lizard' | 'gocyclo'; export function detectComplexityShape(content: string): ComplexityShape { - const trimmed = content.replace(/^/, '').trimStart(); + const trimmed = content.replace(/^\uFEFF/, '').trimStart(); if (trimmed.startsWith('{') || trimmed.startsWith('[')) return 'radon'; // JSON if (trimmed.startsWith('<')) return 'lizard'; // XML return 'gocyclo'; // plain text diff --git a/.github/actions/report/src/goprofile.ts b/.github/actions/report/src/goprofile.ts index 66f8599..175dff3 100644 --- a/.github/actions/report/src/goprofile.ts +++ b/.github/actions/report/src/goprofile.ts @@ -34,8 +34,7 @@ export function parseGoProfile(content: string): CoverageResult { if (count > 0) coveredStatements += numStatements; } - const lineCoverage = - totalStatements > 0 ? round((coveredStatements / totalStatements) * 100) : 0; + const lineCoverage = totalStatements > 0 ? round((coveredStatements / totalStatements) * 100) : 0; return { line_coverage: lineCoverage }; } diff --git a/.github/actions/report/src/index.ts b/.github/actions/report/src/index.ts index 1075a52..afa4f64 100644 --- a/.github/actions/report/src/index.ts +++ b/.github/actions/report/src/index.ts @@ -82,9 +82,7 @@ export async function main(): Promise { } core.info( `Coverage (${format}) from ${coveragePath}: ${coverage.line_coverage}% lines` + - (coverage.branch_coverage !== undefined - ? `, ${coverage.branch_coverage}% branches` - : ''), + (coverage.branch_coverage !== undefined ? `, ${coverage.branch_coverage}% branches` : ''), ); // ── 2. Resolve optional complexity + duplication reports ────────────────── diff --git a/.github/actions/report/src/paths.ts b/.github/actions/report/src/paths.ts index 0075966..68bc65d 100644 --- a/.github/actions/report/src/paths.ts +++ b/.github/actions/report/src/paths.ts @@ -49,7 +49,10 @@ function probe(probes: string[], exists: Exists): string | null { * Resolve the coverage report path. Always required. * @throws if an explicit path is missing, or if probing finds nothing. */ -export function resolveCoveragePath(input: string | undefined, exists: Exists = defaultExists): string { +export function resolveCoveragePath( + input: string | undefined, + exists: Exists = defaultExists, +): string { const trimmed = input?.trim(); if (trimmed) { if (!exists(trimmed)) { diff --git a/.github/actions/report/src/run.ts b/.github/actions/report/src/run.ts index ec7dc61..52734cd 100644 --- a/.github/actions/report/src/run.ts +++ b/.github/actions/report/src/run.ts @@ -82,9 +82,8 @@ export async function report( // Non-default branch pushes are expected — the Worker only persists default-branch data. // Skip before the token mint to avoid a 422 that would fail the job. if (!isPR) { - const defaultBranch = ( - github.context.payload as { repository?: { default_branch?: string } } - ).repository?.default_branch; + const defaultBranch = (github.context.payload as { repository?: { default_branch?: string } }) + .repository?.default_branch; const currentBranch = process.env.GITHUB_REF_NAME ?? ''; if (defaultBranch && currentBranch !== defaultBranch) { core.info(`Not on default branch (${currentBranch} ≠ ${defaultBranch}) — skipping ingest.`); @@ -188,7 +187,9 @@ export async function runPRCheck( const data = (await res.json()) as BaselineResponse; baselines[m.name] = data.value; } catch { - core.warning(`Baseline fetch for "${m.name}" returned non-JSON body (HTTP ${res.status}) — skipping baseline.`); + core.warning( + `Baseline fetch for "${m.name}" returned non-JSON body (HTTP ${res.status}) — skipping baseline.`, + ); } } else if (res.status !== 404) { core.warning(`Baseline fetch for "${m.name}" returned HTTP ${res.status}.`); @@ -228,7 +229,13 @@ export async function runPRCheck( const failed = reasons.length > 0; if (failed) anyFailed = true; - const hasThreshold = thresholdConfigured(m.name, minCoverage, maxCoverageDrop, maxComplexity, maxDuplication); + const hasThreshold = thresholdConfigured( + m.name, + minCoverage, + maxCoverageDrop, + maxComplexity, + maxDuplication, + ); results.push({ metric: m.name, diff --git a/.github/scripts/generate-changelog-entry.mjs b/.github/scripts/generate-changelog-entry.mjs index a13e6c3..9c2590f 100644 --- a/.github/scripts/generate-changelog-entry.mjs +++ b/.github/scripts/generate-changelog-entry.mjs @@ -13,11 +13,10 @@ const RS = '\x1e'; const FS = '\x1f'; const range = prevTag ? `${prevTag}..HEAD` : 'HEAD'; -const raw = execFileSync( - 'git', - ['log', range, '--no-merges', `--pretty=%H${FS}%s${FS}%b${RS}`], - { encoding: 'utf8', maxBuffer: 1024 * 1024 * 32 }, -); +const raw = execFileSync('git', ['log', range, '--no-merges', `--pretty=%H${FS}%s${FS}%b${RS}`], { + encoding: 'utf8', + maxBuffer: 1024 * 1024 * 32, +}); const commits = raw .split(RS) diff --git a/.github/scripts/insert-changelog-entry.mjs b/.github/scripts/insert-changelog-entry.mjs index 1ec6ada..334cc15 100644 --- a/.github/scripts/insert-changelog-entry.mjs +++ b/.github/scripts/insert-changelog-entry.mjs @@ -30,13 +30,8 @@ const linksAndAfter = lines.slice(linkIndex); const newLink = `[${version}]: https://github.com/${repo}/releases/tag/v${version}`; -const output = [ - ...before, - ...section.split('\n'), - '', - ...middle, - newLink, - ...linksAndAfter, -].join('\n'); +const output = [...before, ...section.split('\n'), '', ...middle, newLink, ...linksAndAfter].join( + '\n', +); writeFileSync(changelogPath, output.endsWith('\n') ? output : output + '\n'); diff --git a/.github/workflows/action-test.yml b/.github/workflows/action-test.yml index cc068f6..5bf7301 100644 --- a/.github/workflows/action-test.yml +++ b/.github/workflows/action-test.yml @@ -25,9 +25,21 @@ jobs: # ── 1. Unit-test the Action runner ────────────────────────────────────── # vitest --coverage emits coverage/lcov.info (the lcov reporter). The Action # dogfoods itself: its own real coverage becomes the reported artifact. + - name: Install Action dependencies + working-directory: .github/actions/report + run: npm ci + + - name: Lint Action + working-directory: .github/actions/report + run: npm run lint + + - name: Check Action formatting + working-directory: .github/actions/report + run: npm run format:check + - name: Test Action runner working-directory: .github/actions/report - run: npm ci && npm test + run: npm test # ── 2. Stage the LCOV report at the zero-config probe path ────────────── # coverage/lcov.info (repo root) is probe #2, so the Action auto-detects it diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b6399ad..5b9ec87 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,8 +17,8 @@ jobs: runs-on: ubuntu-latest permissions: contents: read - id-token: write # mint OIDC token for Worker ingest - checks: write # post PR Check Run + id-token: write # mint OIDC token for Worker ingest + checks: write # post PR Check Run defaults: run: working-directory: dashboard @@ -34,6 +34,12 @@ jobs: - name: Install dashboard dependencies run: npm ci + - name: Lint dashboard + run: npm run lint + + - name: Check dashboard formatting + run: npm run format:check + - name: Cache Playwright browsers uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 id: playwright-cache @@ -76,8 +82,8 @@ jobs: runs-on: ubuntu-latest permissions: contents: read - id-token: write # mint OIDC token for Worker ingest - checks: write # post PR Check Run + id-token: write # mint OIDC token for Worker ingest + checks: write # post PR Check Run steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -90,6 +96,12 @@ jobs: - name: Install dependencies run: npm ci + - name: Lint Worker + run: npm run lint + + - name: Check Worker formatting + run: npm run format:check + # vitest.config.mts loads wrangler.json, whose assets.directory points at # dashboard/build (gitignored, absent on a fresh checkout). The Workers pool # tolerates a missing dir for these API-only tests, but create it as a cheap diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 0a0417d..e5bdc26 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -39,7 +39,6 @@ jobs: plugin_marketplaces: 'https://github.com/anthropics/claude-code.git' plugins: 'code-review@claude-code-plugins' prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}' - allowed_bots: "dependabot[bot],copilot[bot]" + allowed_bots: 'dependabot[bot],copilot[bot]' # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md # or https://code.claude.com/docs/en/cli-reference for available options - diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 6b15fac..eb9719e 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -47,4 +47,3 @@ jobs: # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md # or https://code.claude.com/docs/en/cli-reference for available options # claude_args: '--allowed-tools Bash(gh pr *)' - diff --git a/.github/workflows/scancode.yml b/.github/workflows/scancode.yml index de512aa..9c65e2f 100644 --- a/.github/workflows/scancode.yml +++ b/.github/workflows/scancode.yml @@ -34,7 +34,7 @@ jobs: - uses: aboutcode-org/scancode-action@76777db8400d719de67ba3e465c5881037b45cb9 # main, 2026-07-10 with: - pipelines: "scan_codebase" - output-formats: "json spdx cyclonedx" + pipelines: 'scan_codebase' + output-formats: 'json spdx cyclonedx' check-compliance: true - compliance-fail-level: "ERROR" + compliance-fail-level: 'ERROR' diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..b98756a --- /dev/null +++ b/.prettierignore @@ -0,0 +1,11 @@ +node_modules/ +.claude/ +coverage/ +.wrangler/ +dist/ +worker-configuration.d.ts +package-lock.json + +# Formatted independently by their own project (own devDependencies/config) +dashboard/ +.github/actions/report/ diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..4cbc711 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "semi": true, + "singleQuote": true, + "trailingComma": "all", + "printWidth": 100, + "tabWidth": 2 +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 102c62f..f6ef0d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,7 +51,7 @@ always wins). See `docs/generating-coverage-reports.md`. - **(a) The Action no longer runs your tests or coverage tools.** It previously detected the language and ran `go tool cover`, `coverage.py`, Istanbul, etc. - Now you must produce the coverage report yourself *before* the Action step. + Now you must produce the coverage report yourself _before_ the Action step. The `coverage-report-go` / `coverage-report-python` / `coverage-report-js` inputs are removed; use `coverage-path` (or rely on auto-detection). - **(b) jscpd is no longer auto-installed.** Duplication silently disappears for @@ -78,13 +78,13 @@ always wins). See `docs/generating-coverage-reports.md`. ```yaml # You run the tools; the Action reads the report (auto-detected). -- run: npm test -- --coverage # writes coverage/lcov.info -- run: npx jscpd . --reporters json --output ./jscpd-report # only if you want duplication +- run: npm test -- --coverage # writes coverage/lcov.info +- run: npx jscpd . --reporters json --output ./jscpd-report # only if you want duplication - uses: CoverageTracker/coverage-tracker/.github/actions/report@v0.2.0 with: worker-url: https://coverage-tracker.example.com # coverage-path is auto-detected; complexity/duplication are optional - max-duplication: '5' # now requires a jscpd report + max-duplication: '5' # now requires a jscpd report ``` ### Added diff --git a/CLAUDE.md b/CLAUDE.md index 626489e..32e7a25 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ Operating manual for **coverage-tracker**. Read this first every session. For th ## What this is -A self-hostable, open-source (MIT) code-quality dashboard. **One instance per deployer, not multi-tenant.** A single Cloudflare Worker serves the SPA dashboard *and* the API from one apex domain, backed by D1. CI jobs push coverage/complexity metrics; the dashboard reads trends. +A self-hostable, open-source (MIT) code-quality dashboard. **One instance per deployer, not multi-tenant.** A single Cloudflare Worker serves the SPA dashboard _and_ the API from one apex domain, backed by D1. CI jobs push coverage/complexity metrics; the dashboard reads trends. ## Architecture invariants @@ -19,13 +19,13 @@ A self-hostable, open-source (MIT) code-quality dashboard. **One instance per de ## Auth model (per route) -| Route | Edge (Cloudflare Access) | In-code | -|---|---|---| -| Dashboard SPA (`/`, `/dashboard*`) | **Access-protected** | — | -| `/api/health` | none | none (public) | -| `/api/ci/coverage` | none | GitHub Actions **OIDC** (jose, JWKS) | -| `/api/webhooks/github` | none | GitHub App **HMAC** (`X-Hub-Signature-256`) | -| `/api/projects/*` | none | **Cloudflare Access JWT** (`Cf-Access-Jwt-Assertion`, verify `aud`) | +| Route | Edge (Cloudflare Access) | In-code | +| ---------------------------------- | ------------------------ | ------------------------------------------------------------------- | +| Dashboard SPA (`/`, `/dashboard*`) | **Access-protected** | — | +| `/api/health` | none | none (public) | +| `/api/ci/coverage` | none | GitHub Actions **OIDC** (jose, JWKS) | +| `/api/webhooks/github` | none | GitHub App **HMAC** (`X-Hub-Signature-256`) | +| `/api/projects/*` | none | **Cloudflare Access JWT** (`Cf-Access-Jwt-Assertion`, verify `aud`) | ## Guardrails (do not violate) diff --git a/README.md b/README.md index 64500b6..d05a876 100644 --- a/README.md +++ b/README.md @@ -95,12 +95,12 @@ auto-detects the coverage report from the common default paths — zero config: ```yaml # .github/workflows/coverage.yml permissions: - id-token: write # OIDC token for Worker auth - checks: write # PR Check Run + id-token: write # OIDC token for Worker auth + checks: write # PR Check Run steps: - uses: actions/checkout@v4 - - run: npm test -- --coverage # writes coverage/lcov.info + - run: npm test -- --coverage # writes coverage/lcov.info - uses: CoverageTracker/coverage-tracker/.github/actions/report@v0.4.0 with: worker-url: https://coverage-tracker.yourdomain.com @@ -194,17 +194,17 @@ curl -X PATCH https://coverage-tracker.yourdomain.com/api/admin/projects/1/badge ## API reference -| Method | Path | Auth | Description | -|--------|------|------|-------------| -| `POST` | `/api/ci/coverage` | OIDC token | Push typed coverage metrics from CI | -| `GET` | `/api/projects` | Access | List all registered owners and repos | -| `GET` | `/api/projects/:owner/:repo/metrics` | Access | Trend data for one repo | -| `GET` | `/api/baseline/:owner/:repo` | OIDC token | Latest value on default branch (threshold checks) | -| `GET` | `/api/badge/:owner/:repo/:metric.json` | Public | shields.io endpoint — only for `badge_enabled` repos | -| `POST` | `/api/webhooks/github` | GitHub HMAC | GitHub App installation events | -| `POST` | `/api/admin/resync` | Access | Reconcile projects table against GitHub | -| `PATCH` | `/api/admin/projects/:id/badge` | Access | Toggle badge visibility | -| `GET` | `/api/health` | Public | Liveness check | +| Method | Path | Auth | Description | +| ------- | -------------------------------------- | ----------- | ---------------------------------------------------- | +| `POST` | `/api/ci/coverage` | OIDC token | Push typed coverage metrics from CI | +| `GET` | `/api/projects` | Access | List all registered owners and repos | +| `GET` | `/api/projects/:owner/:repo/metrics` | Access | Trend data for one repo | +| `GET` | `/api/baseline/:owner/:repo` | OIDC token | Latest value on default branch (threshold checks) | +| `GET` | `/api/badge/:owner/:repo/:metric.json` | Public | shields.io endpoint — only for `badge_enabled` repos | +| `POST` | `/api/webhooks/github` | GitHub HMAC | GitHub App installation events | +| `POST` | `/api/admin/resync` | Access | Reconcile projects table against GitHub | +| `PATCH` | `/api/admin/projects/:id/badge` | Access | Toggle badge visibility | +| `GET` | `/api/health` | Public | Liveness check | ### `/api/ci/coverage` payload @@ -253,12 +253,12 @@ npm run deploy See [docs/PROGRESS.md](docs/PROGRESS.md) for a full breakdown. Current state: -| Phase | Description | Status | -|-------|-------------|--------| -| 1–6 | Core Worker, webhooks, dashboard, reporting Action | Complete | -| — | Convergence refactor (single Worker + static assets) | Complete | -| 7 | "Deploy to Cloudflare" button | In progress | -| 8 | Docs, OSS hygiene, public release | In progress | +| Phase | Description | Status | +| ----- | ---------------------------------------------------- | ----------- | +| 1–6 | Core Worker, webhooks, dashboard, reporting Action | Complete | +| — | Convergence refactor (single Worker + static assets) | Complete | +| 7 | "Deploy to Cloudflare" button | In progress | +| 8 | Docs, OSS hygiene, public release | In progress | --- diff --git a/dashboard/.prettierignore b/dashboard/.prettierignore new file mode 100644 index 0000000..ff3f8c3 --- /dev/null +++ b/dashboard/.prettierignore @@ -0,0 +1,8 @@ +node_modules/ +.svelte-kit/ +build/ +coverage/ +playwright-report/ +test-results/ +.nyc_output/ +package-lock.json diff --git a/dashboard/.prettierrc.json b/dashboard/.prettierrc.json new file mode 100644 index 0000000..21ebc40 --- /dev/null +++ b/dashboard/.prettierrc.json @@ -0,0 +1,9 @@ +{ + "semi": true, + "singleQuote": true, + "trailingComma": "all", + "printWidth": 100, + "tabWidth": 2, + "plugins": ["prettier-plugin-svelte"], + "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] +} diff --git a/dashboard/eslint.config.js b/dashboard/eslint.config.js new file mode 100644 index 0000000..b0883ad --- /dev/null +++ b/dashboard/eslint.config.js @@ -0,0 +1,29 @@ +import js from '@eslint/js'; +import tseslint from 'typescript-eslint'; +import svelte from 'eslint-plugin-svelte'; +import eslintConfigPrettier from 'eslint-config-prettier'; +import globals from 'globals'; + +export default tseslint.config( + { + ignores: [ + '.svelte-kit/**', + 'build/**', + 'coverage/**', + 'playwright-report/**', + 'test-results/**', + '.nyc_output/**', + 'node_modules/**', + ], + }, + js.configs.recommended, + ...tseslint.configs.recommended, + ...svelte.configs['flat/recommended'], + { languageOptions: { globals: { ...globals.browser } } }, + { + files: ['**/*.svelte'], + languageOptions: { parserOptions: { parser: tseslint.parser } }, + }, + eslintConfigPrettier, + ...svelte.configs['flat/prettier'], +); diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index c72f8db..f5a4e52 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -14,13 +14,21 @@ }, "devDependencies": { "@axe-core/playwright": "^4.11.3", + "@eslint/js": "^9.0.0", "@playwright/test": "^1.49.1", "@sveltejs/adapter-static": "^3.0.10", "@sveltejs/vite-plugin-svelte": "^7.1.2", "@types/node": "^26.0.0", "cross-env": "^10.1.0", + "eslint": "^9.0.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-svelte": "^2.46.0", + "globals": "^15.9.0", "nyc": "^18.0.0", + "prettier": "^3.3.0", + "prettier-plugin-svelte": "^3.2.0", "typescript": "^5.6.0", + "typescript-eslint": "^8.0.0", "vite": "^8.1.0", "vite-plugin-istanbul": "^9.0.1", "wrangler": "^4.104.0" @@ -895,6 +903,352 @@ "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.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", + "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.0", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/@eslint/eslintrc/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.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/@img/colour": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", @@ -1986,6 +2340,13 @@ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "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/@types/node": { "version": "26.1.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.0.tgz", @@ -2002,76 +2363,323 @@ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "license": "MIT" }, - "node_modules/acorn": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", - "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.63.0.tgz", + "integrity": "sha512-rvwSgqT+DHpWdzfSzPatRLm02a0GlESt++9iy3hLCDY4BgkaLcl8LBi9Yh7XGFBpwcBE/K3024QuXWTpbz4FfQ==", + "dev": true, "license": "MIT", - "bin": { - "acorn": "bin/acorn" + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.63.0", + "@typescript-eslint/type-utils": "8.63.0", + "@typescript-eslint/utils": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" }, "engines": { - "node": ">=0.4.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.63.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.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==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", "dev": true, "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "engines": { + "node": ">= 4" } }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "node_modules/@typescript-eslint/parser": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.63.0.tgz", + "integrity": "sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig==", "dev": true, "license": "MIT", "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" + "@typescript-eslint/scope-manager": "8.63.0", + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0", + "debug": "^4.4.3" }, "engines": { - "node": ">=8" + "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.1.0" } }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.63.0.tgz", + "integrity": "sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==", "dev": true, "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.63.0", + "@typescript-eslint/types": "^8.63.0", + "debug": "^4.4.3" + }, "engines": { - "node": ">=8" + "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.1.0" } }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.63.0.tgz", + "integrity": "sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==", "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0" }, "engines": { - "node": ">=8" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/append-transform": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", - "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.63.0.tgz", + "integrity": "sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==", "dev": true, "license": "MIT", - "dependencies": { - "default-require-extensions": "^3.0.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.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.63.0.tgz", + "integrity": "sha512-Nzzh/OGxVCOjObjaj1CQF2RUasyYy2Jfuh+zZ3PjLzG2fYRriAiZLib9UKtO+CpQAS3YHiAS+ckZDclwqI1TPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.0", + "@typescript-eslint/utils": "8.63.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.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.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.63.0.tgz", + "integrity": "sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==", + "devOptional": 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.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.63.0.tgz", + "integrity": "sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.63.0", + "@typescript-eslint/tsconfig-utils": "8.63.0", + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.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.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.63.0.tgz", + "integrity": "sha512-fUKaeAvrTuQg/Tgt3nliAUSZHJM6DlCcfyEmxCvlX8kieWSStBX+5O5Fnidtc3i2JrH+9c/GL4RY2iasd/GPTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.63.0", + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.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.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.63.0.tgz", + "integrity": "sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.63.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/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "license": "MIT", + "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/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "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/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/append-transform": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-require-extensions": "^3.0.0" }, "engines": { "node": ">=8" @@ -2215,6 +2823,16 @@ "node": ">=8" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -2246,6 +2864,36 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/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/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -2304,6 +2952,13 @@ "dev": true, "license": "MIT" }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, "node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", @@ -2353,6 +3008,19 @@ "node": ">= 8" } }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2381,6 +3049,13 @@ "node": ">=0.10.0" } }, + "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/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -2504,6 +3179,160 @@ "node": ">=6" } }, + "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": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.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", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.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", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-compat-utils": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz", + "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", + "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-svelte": { + "version": "2.46.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.46.1.tgz", + "integrity": "sha512-7xYr2o4NID/f9OEYMqxsEQsCsj4KaMy4q5sANaKkAb6/QeCjYFxRmDm2S3YC3A3pl1kyPZ/syOx/i7LcWYSbIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@jridgewell/sourcemap-codec": "^1.4.15", + "eslint-compat-utils": "^0.5.1", + "esutils": "^2.0.3", + "known-css-properties": "^0.35.0", + "postcss": "^8.4.38", + "postcss-load-config": "^3.1.4", + "postcss-safe-parser": "^6.0.0", + "postcss-selector-parser": "^6.1.0", + "semver": "^7.6.2", + "svelte-eslint-parser": "^0.43.0" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0-0 || ^9.0.0-0", + "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "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", @@ -2517,6 +3346,133 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/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/eslint/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/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/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/eslint/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/esm-env": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", @@ -2555,6 +3511,19 @@ "node": ">=4" } }, + "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/esrap": { "version": "2.2.13", "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.13.tgz", @@ -2572,6 +3541,60 @@ } } }, + "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/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/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", @@ -2589,6 +3612,19 @@ } } }, + "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-cache-dir": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", @@ -2621,6 +3657,27 @@ "node": ">=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.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -2735,6 +3792,32 @@ "url": "https://github.com/sponsors/isaacs" } }, + "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": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -2776,6 +3859,43 @@ "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/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "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", @@ -2796,6 +3916,16 @@ "node": ">=8" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -2803,7 +3933,20 @@ "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" } }, "node_modules/is-reference": { @@ -3016,6 +4159,27 @@ "node": ">=6" } }, + "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/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -3029,6 +4193,16 @@ "node": ">=6" } }, + "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/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -3038,6 +4212,27 @@ "node": ">=6" } }, + "node_modules/known-css-properties": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.35.0.tgz", + "integrity": "sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==", + "dev": true, + "license": "MIT" + }, + "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/lightningcss": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", @@ -3299,6 +4494,16 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/locate-character": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", @@ -3325,6 +4530,13 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3451,6 +4663,13 @@ "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/node-preload": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", @@ -3529,6 +4748,24 @@ "node": ">=12.20.0" } }, + "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": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -3604,6 +4841,19 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3756,6 +5006,141 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/yaml": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss-safe-parser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", + "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.3.3" + } + }, + "node_modules/postcss-scss": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz", + "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-scss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.4.29" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "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/prettier": { + "version": "3.9.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.5.tgz", + "integrity": "sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-svelte": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.5.2.tgz", + "integrity": "sha512-ItFouLvzSFE3ulNl4DKoWM3BGcbDCNVpIyy/Y3F2gC3aNiGLxtFUdffVqO5Z5hhYG+DFT5KULWaxmeFFpdbvaQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "prettier": "^3.0.0", + "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" + } + }, "node_modules/process-on-spawn": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.1.0.tgz", @@ -3769,6 +5154,16 @@ "node": ">=8" } }, + "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/release-zalgo": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", @@ -4074,6 +5469,19 @@ "node": ">=8" } }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/supports-color": { "version": "10.2.2", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", @@ -4114,6 +5522,82 @@ "node": ">=18" } }, + "node_modules/svelte-eslint-parser": { + "version": "0.43.0", + "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz", + "integrity": "sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "postcss": "^8.4.39", + "postcss-scss": "^4.0.9" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + } + } + }, + "node_modules/svelte-eslint-parser/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/svelte-eslint-parser/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/svelte-eslint-parser/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/test-exclude": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-8.0.0.tgz", @@ -4154,6 +5638,19 @@ "node": ">=6" } }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -4161,6 +5658,19 @@ "license": "0BSD", "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/type-fest": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", @@ -4195,6 +5705,30 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.63.0.tgz", + "integrity": "sha512-xgwXyzG4sK9ALkBxbyGkTMMOS+imnW65iPhxCQMK83KhxyoDNW7l+IDqEf9vMdoUidHpOoS967RCq4eMiTexwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.63.0", + "@typescript-eslint/parser": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.0", + "@typescript-eslint/utils": "8.63.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.1.0" + } + }, "node_modules/undici": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", @@ -4259,6 +5793,23 @@ "integrity": "sha512-KIMVnG68zvu5XXUbC4LQEPnhwOxBuLyW1AHtpm6IKTXImkbLgkMy+jabjLgSLMasNuGGzQm/ep3tOkyTxpiQIw==", "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/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, "node_modules/vite": { "version": "8.1.3", "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", @@ -4422,6 +5973,16 @@ "dev": true, "license": "ISC" }, + "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/workerd": { "version": "1.20260701.1", "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260701.1.tgz", @@ -4595,6 +6156,19 @@ "node": ">=6" } }, + "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" + } + }, "node_modules/youch": { "version": "4.1.0-beta.10", "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", diff --git a/dashboard/package.json b/dashboard/package.json index ac51e17..d3b8973 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -8,6 +8,10 @@ "build": "vite build", "preview": "vite preview", "typecheck": "svelte-kit sync && tsc --noEmit", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "format": "prettier --write .", + "format:check": "prettier --check .", "test:e2e": "playwright test", "test:e2e:ui": "playwright test --ui", "test:e2e:coverage": "cross-env VITE_COVERAGE=true playwright test && nyc report --temp-dir=.nyc_output --report-dir=coverage --reporter=lcov" @@ -19,13 +23,21 @@ }, "devDependencies": { "@axe-core/playwright": "^4.11.3", + "@eslint/js": "^9.0.0", "@playwright/test": "^1.49.1", "@sveltejs/adapter-static": "^3.0.10", "@sveltejs/vite-plugin-svelte": "^7.1.2", "@types/node": "^26.0.0", "cross-env": "^10.1.0", + "eslint": "^9.0.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-svelte": "^2.46.0", + "globals": "^15.9.0", "nyc": "^18.0.0", + "prettier": "^3.3.0", + "prettier-plugin-svelte": "^3.2.0", "typescript": "^5.6.0", + "typescript-eslint": "^8.0.0", "vite": "^8.1.0", "vite-plugin-istanbul": "^9.0.1", "wrangler": "^4.104.0" diff --git a/dashboard/src/app.css b/dashboard/src/app.css index 406dca9..7512460 100644 --- a/dashboard/src/app.css +++ b/dashboard/src/app.css @@ -4,7 +4,9 @@ font-weight: 400; font-display: swap; src: url('/fonts/inter-400.woff2') format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; + unicode-range: + U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, + U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } @font-face { font-family: 'Inter'; @@ -12,7 +14,9 @@ font-weight: 500; font-display: swap; src: url('/fonts/inter-500.woff2') format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; + unicode-range: + U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, + U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } @font-face { font-family: 'Inter'; @@ -20,7 +24,9 @@ font-weight: 600; font-display: swap; src: url('/fonts/inter-600.woff2') format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; + unicode-range: + U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, + U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } @font-face { font-family: 'Inter'; @@ -28,7 +34,9 @@ font-weight: 700; font-display: swap; src: url('/fonts/inter-700.woff2') format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; + unicode-range: + U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, + U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } @font-face { @@ -37,7 +45,9 @@ font-weight: 500; font-display: swap; src: url('/fonts/jetbrains-mono-500.woff2') format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; + unicode-range: + U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, + U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } @font-face { font-family: 'JetBrains Mono'; @@ -45,7 +55,9 @@ font-weight: 600; font-display: swap; src: url('/fonts/jetbrains-mono-600.woff2') format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; + unicode-range: + U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, + U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } @font-face { font-family: 'JetBrains Mono'; @@ -53,7 +65,9 @@ font-weight: 700; font-display: swap; src: url('/fonts/jetbrains-mono-700.woff2') format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; + unicode-range: + U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, + U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } /* Default tokens — Catppuccin Mocha (dark). Overridden at runtime by theme.svelte.ts. */ @@ -67,8 +81,8 @@ --primary: #89b4fa; --primary-fg: #11111b; --link: #89b4fa; - --ring: rgba(137,180,250,0.3); - --accent-fill: rgba(137,180,250,0.14); + --ring: rgba(137, 180, 250, 0.3); + --accent-fill: rgba(137, 180, 250, 0.14); --chart-0: #89b4fa; --chart-1: #cba6f7; --chart-2: #94e2d5; @@ -90,10 +104,11 @@ html { background: var(--bg); color: var(--text); /* Smooth theme transitions */ - transition: background 0.25s, color 0.25s; + transition: + background 0.25s, + color 0.25s; } - body { margin: 0; } diff --git a/dashboard/src/app.d.ts b/dashboard/src/app.d.ts index 6285566..a6911e5 100644 --- a/dashboard/src/app.d.ts +++ b/dashboard/src/app.d.ts @@ -1,5 +1,5 @@ declare global { - namespace App {} + namespace App {} } export {}; diff --git a/dashboard/src/lib/components/BadgeModal.svelte b/dashboard/src/lib/components/BadgeModal.svelte index 241d127..4e4e239 100644 --- a/dashboard/src/lib/components/BadgeModal.svelte +++ b/dashboard/src/lib/components/BadgeModal.svelte @@ -119,9 +119,7 @@ } if (e.key === 'Tab') { - const focusable = Array.from( - modalEl.querySelectorAll(focusableSelector), - ); + const focusable = Array.from(modalEl.querySelectorAll(focusableSelector)); if (focusable.length === 0) return; const firstEl = focusable[0]; @@ -204,11 +202,7 @@ {#if !localBadgeEnabled}
Public badge serving is disabled — the badge won't load until enabled. -
@@ -228,21 +222,13 @@ Public badge enabled - {/if} - {#each [ - { key: 'url', label: 'Shields.io URL', value: shieldsUrl }, - { key: 'md', label: 'Markdown', value: markdownSnippet }, - { key: 'rst', label: 'reStructuredText', value: rstSnippet }, - ] as snippet (snippet.key)} + {#each [{ key: 'url', label: 'Shields.io URL', value: shieldsUrl }, { key: 'md', label: 'Markdown', value: markdownSnippet }, { key: 'rst', label: 'reStructuredText', value: rstSnippet }] as snippet (snippet.key)}
@@ -88,11 +89,7 @@ - @@ -126,7 +123,8 @@ style="background:{metricChartColor}28; color:{metricChartColor}" aria-label="{delta >= 0 ? 'up' : 'down'} {Math.abs(delta).toFixed(1)}{unit}" > - {delta >= 0 ? '▲' : '▼'} {delta >= 0 ? '+' : ''}{delta.toFixed(1)}{unit} + {delta >= 0 ? '▲' : '▼'} + {delta >= 0 ? '+' : ''}{delta.toFixed(1)}{unit} {/if} {/if} @@ -138,7 +136,7 @@ { // Catch-all registered first = lowest priority; absorbs unmocked /api/* requests - await page.route('**/api/**', (route) => - route.fulfill({ status: 404, body: 'Not found' }), - ); + await page.route('**/api/**', (route) => route.fulfill({ status: 404, body: 'Not found' })); // Specific routes registered last = highest priority (override the catch-all). // metrics/categories is registered before metrics* since Playwright glob `*` // does not cross `/` — the two never actually collide, but keeping the more @@ -52,7 +50,5 @@ export async function mockApi(page: Page): Promise { await page.route('**/api/projects/testorg/repo/metrics*', (route) => route.fulfill({ json: MOCK_TREND_EMPTY }), ); - await page.route('**/api/projects', (route) => - route.fulfill({ json: [MOCK_PROJECT] }), - ); + await page.route('**/api/projects', (route) => route.fulfill({ json: [MOCK_PROJECT] })); } diff --git a/dashboard/tests/project-detail.spec.ts b/dashboard/tests/project-detail.spec.ts index 7dfe522..e456bcf 100644 --- a/dashboard/tests/project-detail.spec.ts +++ b/dashboard/tests/project-detail.spec.ts @@ -61,7 +61,9 @@ test.describe('project detail page — multiple categories', () => { expect(secondBox!.y).toBeGreaterThanOrEqual(firstBox!.y + firstBox!.height); }); - test('has no WCAG 2.0 AA violations with two stacked category charts rendered', async ({ page }) => { + test('has no WCAG 2.0 AA violations with two stacked category charts rendered', async ({ + page, + }) => { await expect(page.locator('.trend-card')).toHaveCount(2); const results = await new AxeBuilder({ page }) .withTags(['wcag2a', 'wcag2aa']) diff --git a/dashboard/tests/time-range.spec.ts b/dashboard/tests/time-range.spec.ts index a090412..39fc3ff 100644 --- a/dashboard/tests/time-range.spec.ts +++ b/dashboard/tests/time-range.spec.ts @@ -48,11 +48,19 @@ test.describe('project detail page — time range selector', () => { } }); - test('clicking a range button selects it, updates the URL, and refetches with that range', async ({ page }) => { + test('clicking a range button selects it, updates the URL, and refetches with that range', async ({ + page, + }) => { await page.getByRole('tab', { name: '30d', exact: true }).click(); - await expect(page.getByRole('tab', { name: '30d', exact: true })).toHaveAttribute('aria-selected', 'true'); - await expect(page.getByRole('tab', { name: '7d', exact: true })).toHaveAttribute('aria-selected', 'false'); + await expect(page.getByRole('tab', { name: '30d', exact: true })).toHaveAttribute( + 'aria-selected', + 'true', + ); + await expect(page.getByRole('tab', { name: '7d', exact: true })).toHaveAttribute( + 'aria-selected', + 'false', + ); expect(new URL(page.url()).searchParams.get('range')).toBe('30d'); const last = requestedUrls.at(-1); @@ -69,7 +77,10 @@ test.describe('project detail page — time range selector', () => { test('selecting a range does not change the active metric tab', async ({ page }) => { await page.getByRole('tab', { name: '1d', exact: true }).click(); - await expect(page.getByRole('tab', { name: 'Coverage', exact: true })).toHaveAttribute('aria-selected', 'true'); + await expect(page.getByRole('tab', { name: 'Coverage', exact: true })).toHaveAttribute( + 'aria-selected', + 'true', + ); }); }); @@ -84,12 +95,18 @@ test.describe('project detail page — range URL param handling', () => { test('a valid range in the URL pre-selects the matching button', async ({ page }) => { await page.goto('/testorg/repo?range=12h'); await page.waitForSelector('[role="tablist"]'); - await expect(page.getByRole('tab', { name: '12h', exact: true })).toHaveAttribute('aria-selected', 'true'); + await expect(page.getByRole('tab', { name: '12h', exact: true })).toHaveAttribute( + 'aria-selected', + 'true', + ); }); test('an unknown range in the URL falls back to the 7d default', async ({ page }) => { await page.goto('/testorg/repo?range=3weeks'); await page.waitForSelector('[role="tablist"]'); - await expect(page.getByRole('tab', { name: '7d', exact: true })).toHaveAttribute('aria-selected', 'true'); + await expect(page.getByRole('tab', { name: '7d', exact: true })).toHaveAttribute( + 'aria-selected', + 'true', + ); }); }); diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index 66b8e3a..610c833 100644 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -57,9 +57,9 @@ Copy the `database_id` from the output into the `d1_databases` entry of `wrangle { "binding": "DB", "database_name": "coverage", - "database_id": "paste-your-id-here", // ← add this line - "migrations_dir": "migrations" - } + "database_id": "paste-your-id-here", // ← add this line + "migrations_dir": "migrations", + }, ], // ... } @@ -128,7 +128,7 @@ You will create **two** Access applications for the same hostname. The Dashboard > [!WARNING] > **Critical invariant** -> Never put an Access *Allow* policy on `/api/*`. Machine callers (CI OIDC, webhooks, health) must reach the Worker unauthenticated at the edge — API auth is enforced in code. The bypass only removes the edge OAuth redirect; no `/api/*` route is left unprotected. +> Never put an Access _Allow_ policy on `/api/*`. Machine callers (CI OIDC, webhooks, health) must reach the Worker unauthenticated at the edge — API auth is enforced in code. The bypass only removes the edge OAuth redirect; no `/api/*` route is left unprotected. --- diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index d06c1dc..bb05695 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -16,12 +16,14 @@ Tracks completion status for all phases defined in `docs/plans/coverage-tracker- ## Phase 2 — Worker core ✅ Complete ### Auth middleware + - [x] OIDC verification: RS256, pins `iss` + `aud=coverage-tracker`, JWKS cache with refetch-on-unknown-`kid` (A1, A8) - [x] Cloudflare Access JWT verification on all `/api` and `/admin` routes (A2) - [x] GitHub webhook HMAC verification: constant-time compare via `crypto.subtle.verify` (A5) - [x] `workers_dev = false` — no `.workers.dev` bypass (A2) ### Routes + - [x] `POST /ingest` — derives `repository`/`branch`/`sha` from OIDC token claims, not body (A3); INSERT OR IGNORE for idempotency (A11) - [x] `GET /api/projects` — Access-gated - [x] `GET /api/projects/:owner/:repo/metrics` — Access-gated, trend data @@ -29,6 +31,7 @@ Tracks completion status for all phases defined in `docs/plans/coverage-tracker- - [x] `GET /badge/:owner/:repo/:metric.json` — public, shields.io format; returns 404 for `badge_enabled=0` (A12) ### Security + - [x] All D1 queries use `.prepare().bind()` — no string interpolation (A10) - [x] `.dev.vars` gitignored; `.dev.vars.example` committed as template (A9) - [x] `wrangler.json` gitignored; `wrangler.example.jsonc` committed as template @@ -38,17 +41,20 @@ Tracks completion status for all phases defined in `docs/plans/coverage-tracker- ## Phase 3 — GitHub App webhooks ✅ Complete ### Webhook handler + - [x] `POST /webhooks/github` — HMAC-verified, delivery ID dedup (A5) - [x] `installation: created` — upserts owner + all repos - [x] `installation: deleted` — removes all projects for the installation - [x] `installation_repositories: added/removed` — adds/removes individual projects ### Admin / resync + - [x] `performResync()` as a shared function (callable from HTTP and future dashboard) - [x] `POST /admin/resync` — Access-gated, triggers reconciliation against GitHub API - [x] `PATCH /admin/projects/:id/badge` — Access-gated, toggles `badge_enabled` ### Deployment (live) + - [x] Worker deployed to `demo.coveragetracker.dev` - [x] All `wrangler secret`s configured: `GITHUB_APP_ID`, `GITHUB_APP_CLIENT_ID`, `GITHUB_APP_PRIVATE_KEY`, `GITHUB_WEBHOOK_SECRET`, `CF_ACCESS_AUD`, `CF_ACCESS_TEAM_DOMAIN` - [x] GitHub App created, installed on CoverageTracker org (7 repos registered) diff --git a/docs/generating-coverage-reports.md b/docs/generating-coverage-reports.md index 244375e..fd49666 100644 --- a/docs/generating-coverage-reports.md +++ b/docs/generating-coverage-reports.md @@ -15,25 +15,25 @@ Format is detected automatically from file content — you don't set it explicit ## Coverage -| Language | Tool | Format | Command | Default path | -|---|---|---|---|---| -| Go | `go tool cover` | native profile | `go test -coverprofile=coverage.out ./...` | `coverage.out` | -| Python | coverage.py | LCOV | `coverage run -m pytest && coverage lcov -o coverage.lcov` | `coverage.lcov` | -| JS/TS | Istanbul (nyc / vitest / jest) | LCOV | `vitest run --coverage --coverage.reporter=lcov` | `coverage/lcov.info` | -| Rust | cargo-llvm-cov | LCOV | `cargo llvm-cov --lcov --output-path lcov.info` | `lcov.info` | -| C/C++ | gcovr | LCOV | `gcovr --lcov -o coverage.lcov` | `coverage.lcov` | -| C# | coverlet | LCOV | `dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=lcov` | `coverage.info` | -| Java | JaCoCo | JaCoCo XML | `mvn test jacoco:report` (Maven) or `./gradlew jacocoTestReport` (Gradle) | `target/site/jacoco/jacoco.xml` / `build/reports/jacoco/test/jacocoTestReport.xml` | -| Bash | kcov | Cobertura | `kcov --include-path=. coverage/ ./script.sh` | *dynamic — set `coverage-path`* | -| Clojure | Cloverage | LCOV | `lein cloverage --lcov` | `target/coverage/lcov.info` | -| Dart | Flutter test / `coverage` pkg | LCOV | `flutter test --coverage` | `coverage/lcov.info` | -| Elixir | ExCoveralls | LCOV | `mix coveralls.lcov` | `cover/lcov.info` | -| Erlang | covertool | Cobertura | `rebar3 do eunit, cover, covertool generate` | *dynamic — set `coverage-path`* | -| Haskell | hpc + hpc-codecov | LCOV | `cabal test --enable-coverage && hpc-codecov cabal:all -f lcov -o lcov.info` | `lcov.info` | -| Lua | LuaCov + `luacov-reporter-lcov` | LCOV | `luacov -r lcov` | `luacov.report.out` | -| Perl | Devel::Cover + lcov's `perl2lcov` | LCOV | `cover -test && perl2lcov -o coverage.lcov` | `coverage.lcov` | -| PHP | PHPUnit | Cobertura | `XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-cobertura=coverage.xml` | `coverage.xml` | -| Ruby | SimpleCov + `simplecov-lcov` | LCOV | (configure `SimpleCov::Formatter::LcovFormatter` in `spec_helper.rb`) then `rspec` | `coverage/lcov.info` | +| Language | Tool | Format | Command | Default path | +| -------- | --------------------------------- | -------------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | +| Go | `go tool cover` | native profile | `go test -coverprofile=coverage.out ./...` | `coverage.out` | +| Python | coverage.py | LCOV | `coverage run -m pytest && coverage lcov -o coverage.lcov` | `coverage.lcov` | +| JS/TS | Istanbul (nyc / vitest / jest) | LCOV | `vitest run --coverage --coverage.reporter=lcov` | `coverage/lcov.info` | +| Rust | cargo-llvm-cov | LCOV | `cargo llvm-cov --lcov --output-path lcov.info` | `lcov.info` | +| C/C++ | gcovr | LCOV | `gcovr --lcov -o coverage.lcov` | `coverage.lcov` | +| C# | coverlet | LCOV | `dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=lcov` | `coverage.info` | +| Java | JaCoCo | JaCoCo XML | `mvn test jacoco:report` (Maven) or `./gradlew jacocoTestReport` (Gradle) | `target/site/jacoco/jacoco.xml` / `build/reports/jacoco/test/jacocoTestReport.xml` | +| Bash | kcov | Cobertura | `kcov --include-path=. coverage/ ./script.sh` | _dynamic — set `coverage-path`_ | +| Clojure | Cloverage | LCOV | `lein cloverage --lcov` | `target/coverage/lcov.info` | +| Dart | Flutter test / `coverage` pkg | LCOV | `flutter test --coverage` | `coverage/lcov.info` | +| Elixir | ExCoveralls | LCOV | `mix coveralls.lcov` | `cover/lcov.info` | +| Erlang | covertool | Cobertura | `rebar3 do eunit, cover, covertool generate` | _dynamic — set `coverage-path`_ | +| Haskell | hpc + hpc-codecov | LCOV | `cabal test --enable-coverage && hpc-codecov cabal:all -f lcov -o lcov.info` | `lcov.info` | +| Lua | LuaCov + `luacov-reporter-lcov` | LCOV | `luacov -r lcov` | `luacov.report.out` | +| Perl | Devel::Cover + lcov's `perl2lcov` | LCOV | `cover -test && perl2lcov -o coverage.lcov` | `coverage.lcov` | +| PHP | PHPUnit | Cobertura | `XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-cobertura=coverage.xml` | `coverage.xml` | +| Ruby | SimpleCov + `simplecov-lcov` | LCOV | (configure `SimpleCov::Formatter::LcovFormatter` in `spec_helper.rb`) then `rspec` | `coverage/lcov.info` | > [!NOTE] > **Go is parsed directly** @@ -69,12 +69,12 @@ If nothing is found, the Action fails with an error that lists every path it pro Only coverage is required. Complexity and duplication are opt-in: set the path explicitly (`complexity-path` / `duplication-path`), or write the report to the default location below and the Action picks it up automatically. -| Metric | Tool | Command | Default path | -|---|---|---|---| -| Complexity — Go | gocyclo | `gocyclo -avg ./... > gocyclo.txt` | `gocyclo.txt` | -| Complexity — Python | Radon | `radon cc -j . > radon.json` | `radon.json` | -| Complexity — everything else | [Lizard](https://github.com/terryyin/lizard) | `lizard --xml > lizard-report.xml` | `lizard-report.xml` | -| Duplication — any language | [jscpd](https://github.com/kucherenko/jscpd) | `npx jscpd . --reporters json --output ./jscpd-report` | `jscpd-report/jscpd-report.json` | +| Metric | Tool | Command | Default path | +| ---------------------------- | -------------------------------------------- | ------------------------------------------------------ | -------------------------------- | +| Complexity — Go | gocyclo | `gocyclo -avg ./... > gocyclo.txt` | `gocyclo.txt` | +| Complexity — Python | Radon | `radon cc -j . > radon.json` | `radon.json` | +| Complexity — everything else | [Lizard](https://github.com/terryyin/lizard) | `lizard --xml > lizard-report.xml` | `lizard-report.xml` | +| Duplication — any language | [jscpd](https://github.com/kucherenko/jscpd) | `npx jscpd . --reporters json --output ./jscpd-report` | `jscpd-report/jscpd-report.json` | There is no `complexity-tool` input — Radon (JSON), gocyclo (plain text), and Lizard (XML) are recognized by content shape. When probing finds more than one complexity file, precedence is `radon.json` → `gocyclo.txt` → `lizard-report.xml`; an explicit `complexity-path` overrides probing entirely. @@ -90,12 +90,12 @@ There is no `complexity-tool` input — Radon (JSON), gocyclo (plain text), and Cobertura XML is a shared DTD, not an enforced spec — generators disagree on two things the reporter corrects for based on `coverage-tool`: -| `coverage-tool` | Trust `branch-rate`? | Notes | -|---|---|---| -| `gocover-cobertura` | No — always `0` | Go's block-based coverage can't map to branches | -| `kcov` | Yes | | -| `covertool` | Yes | | -| `phpunit` | Yes | | -| `gcovr` | Yes | | +| `coverage-tool` | Trust `branch-rate`? | Notes | +| ------------------- | -------------------- | ----------------------------------------------- | +| `gocover-cobertura` | No — always `0` | Go's block-based coverage can't map to branches | +| `kcov` | Yes | | +| `covertool` | Yes | | +| `phpunit` | Yes | | +| `gcovr` | Yes | | If your `coverage-tool` isn't listed, the reporter treats `branch-rate` as trustworthy by default — open an issue if that's wrong for your generator. diff --git a/docs/plans/badge-category-selection-plan.md b/docs/plans/badge-category-selection-plan.md index a135be8..d27654c 100644 --- a/docs/plans/badge-category-selection-plan.md +++ b/docs/plans/badge-category-selection-plan.md @@ -67,6 +67,7 @@ category row carries all metric columns. ## Wiring: `dashboard/src/routes/[owner]/[repo]/+page.svelte` Pass the new prop at the existing `` call (lines 143–149): + ```svelte ` call (lines 143–149): ## Tests: `test/badge.test.ts` Extend the existing helpers and add cases: + - Extend `seedCoverage()` to accept a `category` field (default `'default'`), inserting into the `category` column. - Extend `getBadge()` to accept an optional `category` and append it as a `?category=` diff --git a/docs/plans/consumer-generated-reports-migration-plan-v3.md b/docs/plans/consumer-generated-reports-migration-plan-v3.md index 0d3a180..d30446c 100644 --- a/docs/plans/consumer-generated-reports-migration-plan-v3.md +++ b/docs/plans/consumer-generated-reports-migration-plan-v3.md @@ -114,31 +114,31 @@ This also removes the old "invoke `collect.sh` via `bash`" workaround. Coverage (in probe order — first hit wins): -| Path | Produced by | -|---|---| -| `coverage.out` | `go tool cover` | -| `coverage/lcov.info` | Istanbul/vitest/jest, SimpleCov, Dart/Flutter | -| `lcov.info` | cargo-llvm-cov, hpc-codecov | -| `coverage.lcov` | coverage.py, gcovr, perl2lcov | -| `coverage.info` | coverlet | -| `cover/lcov.info` | ExCoveralls | -| `target/coverage/lcov.info` | Cloverage | -| `target/site/jacoco/jacoco.xml` | JaCoCo (Maven) | -| `build/reports/jacoco/test/jacocoTestReport.xml` | JaCoCo (Gradle) | -| `coverage.xml` | PHPUnit (Cobertura) | -| `luacov.report.out` | LuaCov lcov reporter | +| Path | Produced by | +| ------------------------------------------------ | --------------------------------------------- | +| `coverage.out` | `go tool cover` | +| `coverage/lcov.info` | Istanbul/vitest/jest, SimpleCov, Dart/Flutter | +| `lcov.info` | cargo-llvm-cov, hpc-codecov | +| `coverage.lcov` | coverage.py, gcovr, perl2lcov | +| `coverage.info` | coverlet | +| `cover/lcov.info` | ExCoveralls | +| `target/coverage/lcov.info` | Cloverage | +| `target/site/jacoco/jacoco.xml` | JaCoCo (Maven) | +| `build/reports/jacoco/test/jacocoTestReport.xml` | JaCoCo (Gradle) | +| `coverage.xml` | PHPUnit (Cobertura) | +| `luacov.report.out` | LuaCov lcov reporter | (kcov and covertool default paths contain glob/dynamic segments — those consumers set `coverage-path` explicitly; document this.) Complexity and duplication: -| Signal | Tool | Default path | -|---|---|---| -| Complexity | Radon | `radon.json` | -| Complexity | gocyclo | `gocyclo.txt` | -| Complexity | Lizard | `lizard-report.xml` | -| Duplication | jscpd | `jscpd-report/jscpd-report.json` | +| Signal | Tool | Default path | +| ----------- | ------- | -------------------------------- | +| Complexity | Radon | `radon.json` | +| Complexity | gocyclo | `gocyclo.txt` | +| Complexity | Lizard | `lizard-report.xml` | +| Duplication | jscpd | `jscpd-report/jscpd-report.json` | Document all of the above as the convention consumers write to for auto-detection. @@ -196,6 +196,7 @@ module. ### Phase 5 — Markdown documentation + - [x] `docs/generating-coverage-reports.md` — **generated file, do not edit directly.** Canonical source is `generating-coverage-reports.svx` in the coveragetracker.dev repo (see Plan B, Phase 2a). Author these @@ -268,7 +269,7 @@ markdown copy is generated from here (Phase 2a). Node toolchain): for each configured `.svx`, parse frontmatter, emit `# {title}` followed by the body verbatim, prefixed with an HTML comment header: `` +src/lib/docs-content/.svx — do not edit here -->` (extended: titled callouts are rewritten to GitHub-renderable alerts, `` blocks are stripped, and multi-source targets compose several `.svx` into one doc — used for `docs/INSTALLATION.md`, @@ -289,6 +290,8 @@ markdown copy is generated from here (Phase 2a). (`COVERAGE_TRACKER_SYNC_TOKEN`). Do **not** widen the product GitHub App's permissions for docs plumbing. **Manual step remaining: generate the PAT and save the secret** — workflow and docs are in place. + + - [x] Update `README.md` in this repo with a "Docs export pipeline" section documenting the PAT setup so the sync can be re-provisioned (token expiry, new fork, new maintainer): diff --git a/docs/plans/coverage-tracker-convergence-plan.md b/docs/plans/coverage-tracker-convergence-plan.md index 25465d4..20bd1cc 100644 --- a/docs/plans/coverage-tracker-convergence-plan.md +++ b/docs/plans/coverage-tracker-convergence-plan.md @@ -13,16 +13,16 @@ Serve the dashboard SPA and the API from the **same apex domain** out of **one W ## 2. Current → Target state -| Aspect | Current | Target | -|---|---|---| -| Frontend hosting | Cloudflare Pages project on apex | Static assets served by the Worker (`assets.directory`) | -| API hosting | Separate Worker(s) / Pages Functions | Same Worker, `/api/*` routed via `run_worker_first` | -| Config files | Pages config + Worker `wrangler.*` | Single `wrangler.json` | -| Domain routing | Pages custom domain + Worker route precedence | One Worker custom domain on the apex | -| Dashboard auth | Cloudflare Access (scoped apps already in place) | Cloudflare Access scoped to dashboard paths only | -| API auth | per-Worker | In-code: OIDC (CI), HMAC (webhooks), none (health) | -| Coverage storage | (new) | `coverage_runs` (pruned) + `coverage_daily` (permanent) | -| Trend computation | (new) | Daily cron: last-of-day rollup → prune | +| Aspect | Current | Target | +| ----------------- | ------------------------------------------------ | ------------------------------------------------------- | +| Frontend hosting | Cloudflare Pages project on apex | Static assets served by the Worker (`assets.directory`) | +| API hosting | Separate Worker(s) / Pages Functions | Same Worker, `/api/*` routed via `run_worker_first` | +| Config files | Pages config + Worker `wrangler.*` | Single `wrangler.json` | +| Domain routing | Pages custom domain + Worker route precedence | One Worker custom domain on the apex | +| Dashboard auth | Cloudflare Access (scoped apps already in place) | Cloudflare Access scoped to dashboard paths only | +| API auth | per-Worker | In-code: OIDC (CI), HMAC (webhooks), none (health) | +| Coverage storage | (new) | `coverage_runs` (pruned) + `coverage_daily` (permanent) | +| Trend computation | (new) | Daily cron: last-of-day rollup → prune | --- @@ -44,7 +44,7 @@ Serve the dashboard SPA and the API from the **same apex domain** out of **one W These touch the Cloudflare dashboard, DNS, secrets, or GitHub config and **cannot be done from the repo**. Claude Code should treat these as preconditions/post-conditions and **not** attempt them. Flag clearly in the cutover phase. -1. **Secrets** (set via `wrangler secret put`, see §8). Claude Code may write the *names* into `wrangler.json`/types but must never commit values. +1. **Secrets** (set via `wrangler secret put`, see §8). Claude Code may write the _names_ into `wrangler.json`/types but must never commit values. 2. **Cloudflare Access apps**: after cutover, scope Access application(s) to the dashboard paths on the apex (e.g. `/`, `/dashboard*`), and ensure **no Access app covers `/api/*`** — machine callers (OIDC CI, webhooks, public health) must reach the Worker unauthenticated at the edge. This is the same scoped-app pattern already adopted; re-verify it against the new single-origin path layout. 3. **Custom domain cutover**: detach the apex from the Pages project, attach it to the Worker (Workers custom domain). Brief downtime window — sequence per §7 Phase 9. 4. **GitHub App / OAuth App**: webhook secret + (if dashboard login uses GitHub OAuth via Access) the OAuth app credentials. Existing config from the locked architecture; verify callback URLs still resolve on the converged origin. @@ -100,18 +100,18 @@ These touch the Cloudflare dashboard, DNS, secrets, or GitHub config and **canno "directory": "./dist", "binding": "ASSETS", "not_found_handling": "single-page-application", - "run_worker_first": ["/api/*"] + "run_worker_first": ["/api/*"], }, "observability": { "enabled": true }, - "triggers": { "crons": ["30 6 * * *"] }, // 06:30 UTC daily + "triggers": { "crons": ["30 6 * * *"] }, // 06:30 UTC daily "d1_databases": [ { "binding": "DB", "database_name": "coverage", "database_id": "", - "migrations_dir": "migrations" - } - ] + "migrations_dir": "migrations", + }, + ], } ``` @@ -124,16 +124,19 @@ Run `wrangler types` after this lands to generate the `Bindings` interface — d Each phase is independently committable with its own acceptance check. Do them in order. ### Phase 0 — Branch & dependencies + - Create branch `feat/converge-worker-assets`. - `npm i hono jose zod`. Confirm `wrangler` is v4+ (Workers Static Assets requires it; Workers Sites is deprecated and must not be used). - **Accept:** `npm ls hono jose zod wrangler` resolves; `wrangler --version` ≥ 4. ### Phase 1 — Wrangler convergence + - Replace existing config with §6. Keep the existing `database_id`. - Verify `migrations/0001_projects.sql` (the `projects` table that webhook registration writes to) exists; `coverage_runs.project_id` FKs into it. - **Accept:** `wrangler deploy --dry-run` succeeds; `wrangler types` emits a `Bindings` type containing `ASSETS: Fetcher` and `DB: D1Database`. ### Phase 2 — Worker skeleton + auth middleware + - `src/worker.ts` exports `{ fetch: app.fetch, scheduled }`. - `src/app.ts`: Hono app, mounts routes, **final catch-all** `app.all('*', c => c.env.ASSETS.fetch(c.req.raw))`. - `src/auth/oidc.ts`: middleware verifying GitHub Actions OIDC JWT via `createRemoteJWKSet('https://token.actions.githubusercontent.com/.well-known/jwks')`, issuer `https://token.actions.githubusercontent.com`, audience from `GITHUB_OIDC_AUDIENCE`. Optionally pin `payload.repository`/`repository_owner`. On failure → `401`. @@ -141,22 +144,26 @@ Each phase is independently committable with its own acceptance check. Do them i - **Accept:** unit tests assert 401 on missing/garbage OIDC token and bad HMAC signature; a non-`/api` request falls through to `ASSETS`. ### Phase 3 — D1 migration + - Author `migrations/0002_coverage.sql` exactly as §9. - Apply: `wrangler d1 migrations apply coverage --local` then `--remote`. - **Accept:** `coverage_runs`, `coverage_daily`, and the three indexes exist; FK to `projects` resolves. ### Phase 4 — Coverage ingest route + - `src/routes/ci.ts`: `POST /api/ci/coverage`, behind OIDC middleware. - Validate body with the zod `CoverageReport` schema (§10). On invalid → `422` with issues. - Upsert into `coverage_runs` on conflict `(project_id, commit_sha)` (§10 SQL). Set `ran_at = unixepoch()`. Return `202`. - **Accept:** integration test (vitest-pool-workers, real D1) — valid payload inserts a row; re-POST of same `(project_id, commit_sha)` updates, does not duplicate; invalid payload → 422; missing OIDC → 401. ### Phase 5 — Scheduled rollup + prune + - `src/db/rollup.ts`: `rollupAndPrune(env)` per §11 — snapshot last-of-day for runs older than `RETENTION_DAYS`, upsert into `coverage_daily`, then `DELETE FROM coverage_runs WHERE ran_at < cutoff`. - `worker.ts` `scheduled` handler wraps it in `ctx.waitUntil(...)`. - **Accept:** test seeds runs spanning >14 days with multiple runs/day; after `rollupAndPrune`, each old `(project, day)` has exactly one `coverage_daily` row equal to that day's latest run with correct `run_count`; old raw rows are gone; rows inside the window remain. Re-running is idempotent (no dup snapshots, no error). ### Phase 6 — Dashboard read path + - `src/routes/dashboard-data.ts`: `GET /api/projects/:id/trend` returning a single ordered series that **unions** recent fine-grained points from `coverage_runs` with historical points from `coverage_daily` (recent window from runs, older from daily), ordered by day ascending. De-dup the boundary day in favor of the rollup. - This endpoint is under `/api/*` so it bypasses edge Access. **It must be gated in-code** by an Access JWT middleware (`src/auth/access.ts`) applied to all `/api/projects/*` routes: - Read the `Cf-Access-Jwt-Assertion` header (fall back to the `CF_Authorization` cookie). Missing → `401`. @@ -166,11 +173,13 @@ Each phase is independently committable with its own acceptance check. Do them i - **Accept:** test with seeded runs + daily rows returns a contiguous, correctly ordered series with no duplicate boundary day. A request with no/invalid `Cf-Access-Jwt-Assertion` is rejected (401/403); a request with a valid Access assertion succeeds. ### Phase 7 — Tests + - Use `@cloudflare/vitest-pool-workers` so tests run in the Workers runtime with real D1 bindings. - Confirm `nodejs_compat` is present in `wrangler.json` (the pool injects it for tests, masking a missing flag at deploy — verify explicitly). - **Accept:** `npm test` green; coverage of auth, ingest, rollup, routing. ### Phase 8 — CI ingest workflow + - Add a GitHub Actions step (reusable) that: - requests an OIDC token with `permissions: id-token: write` and the configured audience, - parses the project's coverage report into the `CoverageReport` shape, @@ -178,12 +187,15 @@ Each phase is independently committable with its own acceptance check. Do them i - **Accept:** dry-run against a preview deployment ingests a row; dashboard trend reflects it. ### Phase 9 — Cutover (sequenced; HUMAN executes the dashboard parts) + Order matters to minimize downtime: + 1. Deploy the converged Worker **without** a custom domain (gets a `*.workers.dev` URL). Smoke-test SPA + all three API tiers there. 2. **Human:** in Cloudflare Access, create/scope the dashboard Access app to the apex dashboard paths; confirm no app matches `/api/*`. 3. **Human:** detach the apex from the Pages project; attach it as a Worker custom domain. 4. Verify DNS/edge propagation; smoke-test the apex: dashboard prompts Access, `/api/health` is open, `/api/ci/coverage` accepts OIDC, webhook route validates HMAC. 5. **Human:** once green, delete or archive the old Pages project and any now-dead Worker routes. + - **Accept:** end-to-end checklist (§12) passes on the apex. --- @@ -192,14 +204,14 @@ Order matters to minimize downtime: Set via `wrangler secret put ` (never committed). Reference by the same name in `Bindings`. -| Name | Purpose | -|---|---| -| `GITHUB_OIDC_AUDIENCE` | Expected `aud` of the Actions OIDC token | -| `GITHUB_WEBHOOK_SECRET` | HMAC key for GitHub App webhook signature | -| `CF_ACCESS_TEAM_DOMAIN` | Access team domain, e.g. `.cloudflareaccess.com` (JWKS + issuer) | -| `CF_ACCESS_AUD` | Access application AUD tag, checked as the JWT `aud` for `/api/projects/*` | -| `GITHUB_APP_*` | (existing) GitHub App credentials for project registration, as already defined | -| OAuth/Access | Managed in Access + GitHub OAuth app config, not in the Worker | +| Name | Purpose | +| ----------------------- | ------------------------------------------------------------------------------ | +| `GITHUB_OIDC_AUDIENCE` | Expected `aud` of the Actions OIDC token | +| `GITHUB_WEBHOOK_SECRET` | HMAC key for GitHub App webhook signature | +| `CF_ACCESS_TEAM_DOMAIN` | Access team domain, e.g. `.cloudflareaccess.com` (JWKS + issuer) | +| `CF_ACCESS_AUD` | Access application AUD tag, checked as the JWT `aud` for `/api/projects/*` | +| `GITHUB_APP_*` | (existing) GitHub App credentials for project registration, as already defined | +| OAuth/Access | Managed in Access + GitHub OAuth app config, not in the Worker | --- @@ -247,6 +259,7 @@ CREATE TABLE IF NOT EXISTS coverage_daily ( ## 10. Ingest contract **zod schema:** + ```ts const CoverageReport = z.object({ projectId: z.string().min(1), @@ -258,10 +271,11 @@ const CoverageReport = z.object({ cognitive: z.number().optional(), duplicationPct: z.number().min(0).max(100).optional(), maintainability: z.number().optional(), -}) +}); ``` **Upsert:** + ```sql INSERT INTO coverage_runs (project_id, commit_sha, branch, ran_at, diff --git a/docs/plans/coverage-tracker-plan.md b/docs/plans/coverage-tracker-plan.md index cf92f12..06bd0ee 100644 --- a/docs/plans/coverage-tracker-plan.md +++ b/docs/plans/coverage-tracker-plan.md @@ -42,16 +42,16 @@ Keep this model front-of-mind: it is the reason auth and data isolation stay sim Phases, in eventual build order. **Only Phase 1–3 (core) are in scope right now**; the rest are documented so current designs accommodate them. -| Phase | Scope | Status | -|-------|-------|--------| -| 1 | D1 schema (multi-project from day one) | ✅ complete | -| 2 | Worker core: ingest (OIDC-verified), metrics read, badge | ✅ complete | -| 3 | GitHub App registration webhooks → projects table | ✅ complete | -| 4 | Thresholds + PR diff checks (reporting Action logic) | ✅ complete | -| 5 | Svelte dashboard on Cloudflare Pages, behind Access | not started | -| 6 | Reusable composite reporting Action (in this repo) | ✅ complete | -| 7 | "Deploy to Cloudflare" button + one-click onboarding | not started | -| 8 | Docs, OSS hygiene, public release | 🔶 in progress | +| Phase | Scope | Status | +| ----- | -------------------------------------------------------- | -------------- | +| 1 | D1 schema (multi-project from day one) | ✅ complete | +| 2 | Worker core: ingest (OIDC-verified), metrics read, badge | ✅ complete | +| 3 | GitHub App registration webhooks → projects table | ✅ complete | +| 4 | Thresholds + PR diff checks (reporting Action logic) | ✅ complete | +| 5 | Svelte dashboard on Cloudflare Pages, behind Access | not started | +| 6 | Reusable composite reporting Action (in this repo) | ✅ complete | +| 7 | "Deploy to Cloudflare" button + one-click onboarding | not started | +| 8 | Docs, OSS hygiene, public release | 🔶 in progress | --- @@ -92,7 +92,7 @@ are documented so current designs accommodate them. because the tool is public and self-deployable: without the registered-project check, any repo's CI could push fake data. **No static ingest secret** anywhere. 3. **Project registration (GitHub App installation)** — a lightweight GitHub App that the - deployer installs on the repos/orgs they want tracked. Installing the App *is* the + deployer installs on the repos/orgs they want tracked. Installing the App _is_ the registration step. Its webhooks populate the `owners`/`projects` tables. --- @@ -150,6 +150,7 @@ CREATE INDEX idx_metrics_latest ``` Design notes: + - Key on GitHub **numeric ids** (`github_id`, `github_repo_id`), not logins/slugs, so renames don't orphan data. Keep `full_slug` denormalized only as a fast lookup path from the OIDC `repository` claim. @@ -165,16 +166,17 @@ Design notes: Router skeleton. Each route's auth requirement is explicit — **enforce it, don't assume**. -| Method | Path | Auth | Purpose | -|--------|------|------|---------| -| POST | `/ingest` | OIDC + project check | Insert a metrics datapoint (push on main) | -| GET | `/api/projects` | Access (browser session) | List owners → repos for dashboard | -| GET | `/api/projects/:owner/:repo/metrics` | Access | Trend data for one repo | -| GET | `/api/projects/:owner/:repo/baseline` | OIDC + project check | Baseline value for threshold/PR checks | -| GET | `/badge/:owner/:repo/:metric.json` | **public** | Single number, shields.io endpoint format | -| POST | `/webhooks/github` | GitHub webhook HMAC | App install events → owners/projects sync | +| Method | Path | Auth | Purpose | +| ------ | ------------------------------------- | ------------------------ | ----------------------------------------- | +| POST | `/ingest` | OIDC + project check | Insert a metrics datapoint (push on main) | +| GET | `/api/projects` | Access (browser session) | List owners → repos for dashboard | +| GET | `/api/projects/:owner/:repo/metrics` | Access | Trend data for one repo | +| GET | `/api/projects/:owner/:repo/baseline` | OIDC + project check | Baseline value for threshold/PR checks | +| GET | `/badge/:owner/:repo/:metric.json` | **public** | Single number, shields.io endpoint format | +| POST | `/webhooks/github` | GitHub webhook HMAC | App install events → owners/projects sync | Critical auth notes: + - `/badge/...` is the **only** intentionally-public data route, and it returns exactly one number in shields.io endpoint format. Everything else with real data is gated. - `/api/projects/:owner/:repo/baseline` must be **OIDC-gated, project-scoped** — NOT public. @@ -193,9 +195,9 @@ Critical auth notes: "branch": "main", "commit_sha": "abc123...", "metrics": [ - { "name": "coverage", "value": 82.4, "unit": "%" }, - { "name": "complexity", "value": 4.2, "unit": "score" }, - { "name": "duplication", "value": 1.8, "unit": "%" } + { "name": "coverage", "value": 82.4, "unit": "%" }, + { "name": "complexity", "value": 4.2, "unit": "score" }, + { "name": "duplication", "value": 1.8, "unit": "%" } ] } ``` @@ -251,16 +253,16 @@ Not in the current core build, but the schema and routes above must already supp - **Composite reporting Action**: in this repo at `.github/actions/report/`. Wraps the collection script + OIDC POST. Inputs: `worker-url`, threshold knobs. Version-locked to the Worker by tagging the whole repo together. **Testing strategy (three layers, decided):** - - *Layer 1 (prerequisite):* vitest unit tests for the pure helpers in `src/run.ts` + - _Layer 1 (prerequisite):_ vitest unit tests for the pure helpers in `src/run.ts` (`parseThreshold`, `buildSummary`, etc.). vitest's `json-summary` coverage reporter emits `coverage/coverage-summary.json` in the Istanbul shape `collect.sh` already parses — the Action dogfoods itself, and the self-test workflow reads real coverage instead of a hardcoded fake. - - *Layer 2:* a committed bash fixture script (`test/collect-parsers.sh`) that pipes sample + - _Layer 2:_ a committed bash fixture script (`test/collect-parsers.sh`) that pipes sample tool output through each inline Python parser in `collect.sh` and asserts the result. This is the only way to cover the Go/Python/lizard parser branches, since those tools are absent from this repo's CI environment. - - *Layer 3 (follow-on):* `@cloudflare/vitest-pool-workers` Worker route tests — OIDC + - _Layer 3 (follow-on):_ `@cloudflare/vitest-pool-workers` Worker route tests — OIDC middleware rejection cases, ingest idempotency, baseline gating. Real setup cost; does not block the self-test. - Self-test workflow: `.github/workflows/action-test.yml` in this repo, using the local @@ -317,10 +319,11 @@ of endpoints or payload shapes. Findings are ordered by severity. The plan says "verify signature against JWKS + check `repository`/`ref`." That is necessary but **not sufficient**. A GitHub Actions OIDC token is a general-purpose credential; its default -audience is the repo owner's URL, and the *same* repo's CI could mint a validly-signed token for -some *other* service and have it replayed against `/ingest`. +audience is the repo owner's URL, and the _same_ repo's CI could mint a validly-signed token for +some _other_ service and have it replayed against `/ingest`. Required, in addition to signature: + - **Pin `iss`** to exactly `https://token.actions.githubusercontent.com`. Reject anything else. - **Require a custom `aud`** — set a fixed audience (e.g. the deployer's Worker URL or a constant like `coverage-tracker`) and have the Action request that exact audience via @@ -331,12 +334,13 @@ Required, in addition to signature: ### A2. (CRITICAL) Cloudflare Access only protects the hostname — the raw `workers.dev` URL bypasses it -Access gates the *route/hostname* it's configured on. The underlying `*.workers.dev` URL (and any +Access gates the _route/hostname_ it's configured on. The underlying `*.workers.dev` URL (and any unprotected custom route) stays directly reachable, so anyone hitting the Worker URL directly skips Access entirely and reaches `/api/projects`, `/api/.../metrics`, etc. — all the gated, private coverage data. Fix (do both): + - In the Worker, **verify the `Cf-Access-Jwt-Assertion` header** on every Access-gated route: validate it against your Access application's public keys (the team's `/cdn-cgi/access/certs`) and check the `aud` matches your Access app. Do not trust "Access is in front of me." @@ -356,6 +360,7 @@ refs for the persist-on-default-branch rule rather than mis-parsing a tag as a b Minting installation tokens (for resync and Check Runs) requires the App private key. Two requirements the plan currently omits: + - Store it **only** as a `wrangler secret`, never in source (public repo — a committed key is a permanent leak even after deletion; rotation would be mandatory). - **Scope App permissions minimally.** Registration needs only repository `metadata: read`. @@ -368,6 +373,7 @@ requirements the plan currently omits: `/webhooks/github` is public; the HMAC is its only gate, and it is load-bearing (a forged `installation: created` could register arbitrary repos, which then authorizes those repos' OIDC tokens to ingest). Two pitfalls: + - Compare the computed and received signatures with a **timing-safe** comparison (`crypto.subtle` / equivalent), never `===` — naive comparison is a known signature-bypass vector. - Consider lightweight **replay protection** by deduping on GitHub's `X-GitHub-Delivery` id, so a @@ -375,10 +381,10 @@ tokens to ingest). Two pitfalls: ### A6. (MEDIUM) The manual resync path needs a defined auth model -The plan leaves resync as "an authenticated endpoint *or* a wrangler script." As an unauthenticated +The plan leaves resync as "an authenticated endpoint _or_ a wrangler script." As an unauthenticated HTTP endpoint it's an abuse vector (forces installation-token API calls, can rewrite the projects table). Pick one: make it a **`wrangler`-invoked script** that uses bindings directly with no public -HTTP surface (preferred), or if it must be HTTP, gate it behind Access *and* verify the Access JWT +HTTP surface (preferred), or if it must be HTTP, gate it behind Access _and_ verify the Access JWT per A2. ### A7. (MEDIUM) Rate-limit the public and crypto-heavy routes @@ -415,7 +421,7 @@ so replays produce duplicate datapoints that skew trends. Add a unique constrain `(project_id, commit_sha, metric_name)` so re-ingesting the same commit's metric is a no-op rather than a duplicate. -### A12. (DESIGN DECISION) The badge endpoint *is* public coverage data +### A12. (DESIGN DECISION) The badge endpoint _is_ public coverage data Section 10 asserts "coverage data is private" while `/badge` is public. These are in tension: a badge is, by definition, the latest coverage number exposed publicly, and `/badge/:owner/:repo/:metric` diff --git a/docs/plans/service-auth-token.md b/docs/plans/service-auth-token.md index 5809a56..1322908 100644 --- a/docs/plans/service-auth-token.md +++ b/docs/plans/service-auth-token.md @@ -14,6 +14,7 @@ Service Token, and the end-user's GitHub OAuth auth would continue to protect th (handled by Access, not by the dashboard's code). Benefits over the current approach: + - No JWT forwarding — the user's browser session token is never sent to the Worker - Works identically in local dev and production (just set the env vars) - Removes the `ENVIRONMENT` workaround and its associated risk @@ -24,6 +25,7 @@ Benefits over the current approach: ### 1. Cloudflare Access: create a Service Token In the Cloudflare Zero Trust dashboard: + - **Access → Service Auth → Service Tokens → Create Service Token** - Name: `coverage-tracker-dashboard` - Copy the generated `Client ID` and `Client Secret` (the secret is only shown once) @@ -56,6 +58,7 @@ if ( ``` Add to `Bindings` in `src/types.ts`: + ```typescript CF_ACCESS_CLIENT_ID?: string; CF_ACCESS_CLIENT_SECRET?: string; @@ -92,6 +95,7 @@ pass them to `buildAuthHeaders`. Drop the `ENVIRONMENT` reads. ### 4. Dashboard: add Service Token env vars **File: `dashboard/.env.example`** — add: + ``` CF_ACCESS_CLIENT_ID= CF_ACCESS_CLIENT_SECRET= @@ -103,6 +107,7 @@ In local dev (`.env` or `.dev.vars`), set both. ### 5. Remove the dev bypass Once Service Tokens are wired up, the `ENVIRONMENT` bypass is no longer needed: + - Remove `ENVIRONMENT` check from `src/middleware/access.ts` - Remove `ENVIRONMENT` from `src/types.ts` - Remove `ENVIRONMENT` from both `.dev.vars.example` files @@ -122,7 +127,7 @@ Once Service Tokens are wired up, the `ENVIRONMENT` bypass is no longer needed: - The Service Token secret is a Wrangler secret (not a `vars` entry) — it must never appear in source or `wrangler.json`. -- If the Worker later exposes routes that need to identify *which user* is making the +- If the Worker later exposes routes that need to identify _which user_ is making the request (e.g., per-user data isolation), the JWT forwarding path would need to be reinstated for those routes. This tool only tracks a single deployer's data, so service-level auth is sufficient. diff --git a/docs/plans/trend-chart-time-windows-plan.md b/docs/plans/trend-chart-time-windows-plan.md index c3465b5..e1eff9e 100644 --- a/docs/plans/trend-chart-time-windows-plan.md +++ b/docs/plans/trend-chart-time-windows-plan.md @@ -7,7 +7,7 @@ Two visual bugs showed up once multi-category projects became common: 1. **Project detail page** (`[owner]/[repo]/+page.svelte`): a category with very little history (in the extreme, a single recorded point) renders as a lone dot near the left edge of an otherwise-empty chart, with the x-axis auto-ranging out to whatever `uPlot`'s default ranging - picks (observed spanning *years* into the future on a single-point series). There's currently + picks (observed spanning _years_ into the future on a single-point series). There's currently no way to bound the chart to a sensible window — it always fetches the last 100 rows (`+page.ts:18`, hardcoded `limit: 100`) with no time floor. 2. **Project list page** (homepage cards): the new multi-category overlay chart @@ -28,7 +28,7 @@ The fix is a **relative time-range window with edge-anchoring**, applied server- - The list page's overlay sparkline always uses a fixed **30d** window, with lines aligned to a shared right edge so multi-category cards look consistent regardless of each category's actual reporting cadence. -- In both cases, when there's no real data at the window's start, the closest point *before* the +- In both cases, when there's no real data at the window's start, the closest point _before_ the window is used to synthesize a starting value at the window boundary, so a line is drawn across the full width instead of leaving empty space (down to the degenerate single-point case, which becomes a flat line spanning the whole window with the one real point pinned at the right edge). @@ -36,7 +36,7 @@ The fix is a **relative time-range window with edge-anchoring**, applied server- **Key implementation insight:** none of `TrendChart.svelte`, `SparkLine.svelte`, or `MultiSparkLine.svelte` need any x-domain/scaling logic changes. They already size the x-axis to the exact min/max of whatever `[timestamps, values]` array they're given. The entire fix is about -*what data the backend returns* — inject a synthetic anchor point at the window boundary, and the +_what data the backend returns_ — inject a synthetic anchor point at the window boundary, and the existing charts automatically render full-width. This keeps the chart components dumb and the windowing/anchoring logic in one place (the DB layer), consistent with the "server-side" approach already agreed on for D1 row-scan efficiency (see `idx_runs_project_time` usage below). @@ -51,7 +51,7 @@ For a given category and a requested duration `D`: - **Window start** = `latestTs - D`. - **Anchor point**: the most recent row with `ran_at <= windowStart` (checking raw `coverage_runs` first, then `coverage_daily` for windows that reach past the 14-day `RETENTION_DAYS` cutoff in - `src/db/rollup.ts:3`). Its value is re-emitted as a synthetic point *at* `windowStart` (i.e. + `src/db/rollup.ts:3`). Its value is re-emitted as a synthetic point _at_ `windowStart` (i.e. `recorded_at` overridden to the window boundary, not its original timestamp). If no such row exists at all (window covers this category's entire history), fall back to the **earliest** available row's value as the anchor — for a category with exactly one point ever, this @@ -64,7 +64,7 @@ For a given category and a requested duration `D`: category's line to span the same visual width, even if categories report on different cadences. So for the list page only, compute a **shared right edge** = `MAX(latestTs)` across all of the project's categories, and `windowStart = sharedRightEdge - 30d`. Each category is anchored on the -left as above (relative to the shared window), *and* if a category's own latest point is earlier +left as above (relative to the shared window), _and_ if a category's own latest point is earlier than the shared right edge, a second synthetic point is appended at the shared right edge repeating its last known value (forward-carry), so every series' data spans the exact same `[windowStart, sharedRightEdge]` domain and `uPlot.join` no longer produces partial-width lines. @@ -85,9 +85,10 @@ day** for 15m/1h/12h (and for 12h whenever the window doesn't cross midnight) identical x values and reproducing the exact "single dot" bug this plan exists to fix. Resolution — **granularity policy split by window size**: + - **`< 1 day` windows (15m, 1h, 12h)**: query raw `coverage_runs` only, with full-precision timestamps emitted as ISO 8601 datetimes (`strftime('%Y-%m-%dT%H:%M:%SZ', ran_at, - 'unixepoch')` — parses correctly with `new Date()`), filtered directly by +'unixepoch')` — parses correctly with `new Date()`), filtered directly by `ran_at BETWEEN windowStart AND latestTs`. No day-collapsing, no `coverage_daily` involved under normal operation. The synthetic anchor's `recorded_at` is likewise stamped as a full ISO datetime derived from `windowStart`, not a day string. @@ -126,7 +127,7 @@ indexed queries per category plus straightforward TS. - detail page (`align: false` — independent per-category windows), and - list page (`align: true` — shared right edge + forward-carry, as described above). - Both build on the existing `idx_runs_project_time` index (`project_id, branch, category, - ran_at` — `migrations/0003_categories.sql:17-19`), which fully covers the +ran_at` — `migrations/0003_categories.sql:17-19`), which fully covers the project+branch+category filter plus the `ran_at` range scan for the "in-window rows" and "anchor row" queries. - Reuse `pickMetricValue`/`metricToColumn` from `src/lib/metrics.ts` — unchanged. @@ -188,7 +189,7 @@ files first and follow their seeding conventions): - A `30d` window that spans past the 14-day raw retention boundary correctly sources the older anchor from `coverage_daily` when `coverage_runs` has already been pruned. - **Regression guard for the `recorded_at` trap**: for a `15m`/`1h`/`12h` window, assert the - anchor point and the latest point have *distinct* timestamps (not just distinct values) — this + anchor point and the latest point have _distinct_ timestamps (not just distinct values) — this is exactly the bug the day-collapsed format would silently reintroduce. - `align: true` with two categories having non-overlapping recent history: both series end up spanning the identical `[windowStart, sharedRightEdge]` domain, with the staler category's diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..504189a --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,26 @@ +import js from '@eslint/js'; +import tseslint from 'typescript-eslint'; +import eslintConfigPrettier from 'eslint-config-prettier'; +import globals from 'globals'; + +export default tseslint.config( + { + ignores: [ + 'dist/**', + 'coverage/**', + '.wrangler/**', + 'node_modules/**', + 'worker-configuration.d.ts', + 'dashboard/**', + '.github/actions/report/**', + '.claude/**', + ], + }, + js.configs.recommended, + ...tseslint.configs.recommended, + { + files: ['scripts/**/*.mjs', '.github/scripts/**/*.mjs'], + languageOptions: { globals: { ...globals.node } }, + }, + eslintConfigPrettier, +); diff --git a/package-lock.json b/package-lock.json index 7105060..b74c4d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,9 +14,15 @@ }, "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.16.20", + "@eslint/js": "^9.0.0", "@types/node": "^26.0.1", "@vitest/coverage-istanbul": "^4.1.9", + "eslint": "^9.0.0", + "eslint-config-prettier": "^9.1.0", + "globals": "^15.9.0", + "prettier": "^3.3.0", "typescript": "^5.6.3", + "typescript-eslint": "^8.0.0", "vitest": "^4.1.9", "wrangler": "^4.104.0" } @@ -957,6 +963,229 @@ "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.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", + "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.0", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.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/@img/colour": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", @@ -1980,6 +2209,13 @@ "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/@types/node": { "version": "26.1.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.0.tgz", @@ -1990,176 +2226,528 @@ "undici-types": "~8.3.0" } }, - "node_modules/@vitest/coverage-istanbul": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/coverage-istanbul/-/coverage-istanbul-4.1.10.tgz", - "integrity": "sha512-AyNJ5pQRFqCX7pwB9PSTmoVKPaZ4H5IEVJfJsT+q1DYkXvZMEFYgJlyk5sfStmt9rVYRyYYRRsuBeImCOc39ww==", + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.63.0.tgz", + "integrity": "sha512-rvwSgqT+DHpWdzfSzPatRLm02a0GlESt++9iy3hLCDY4BgkaLcl8LBi9Yh7XGFBpwcBE/K3024QuXWTpbz4FfQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.29.0", - "@istanbuljs/schema": "^0.1.3", - "@jridgewell/gen-mapping": "^0.3.13", - "@jridgewell/trace-mapping": "0.3.31", - "istanbul-lib-coverage": "^3.2.2", - "istanbul-lib-report": "^3.0.1", - "istanbul-reports": "^3.2.0", - "magicast": "^0.5.2", - "obug": "^2.1.1", - "tinyrainbow": "^3.1.0" + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.63.0", + "@typescript-eslint/type-utils": "8.63.0", + "@typescript-eslint/utils": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "vitest": "4.1.10" + "@typescript-eslint/parser": "^8.63.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@vitest/coverage-istanbul/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==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "engines": { + "node": ">= 4" } }, - "node_modules/@vitest/expect": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", - "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "node_modules/@typescript-eslint/parser": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.63.0.tgz", + "integrity": "sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig==", "dev": true, "license": "MIT", "dependencies": { - "@standard-schema/spec": "^1.1.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.10", - "@vitest/utils": "4.1.10", - "chai": "^6.2.2", - "tinyrainbow": "^3.1.0" + "@typescript-eslint/scope-manager": "8.63.0", + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@vitest/mocker": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", - "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.63.0.tgz", + "integrity": "sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.10", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" + "@typescript-eslint/tsconfig-utils": "^8.63.0", + "@typescript-eslint/types": "^8.63.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@vitest/pretty-format": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", - "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.63.0.tgz", + "integrity": "sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^3.1.0" + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@vitest/runner": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", - "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.63.0.tgz", + "integrity": "sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==", "dev": true, "license": "MIT", - "dependencies": { - "@vitest/utils": "4.1.10", - "pathe": "^2.0.3" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@vitest/snapshot": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", - "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "node_modules/@typescript-eslint/type-utils": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.63.0.tgz", + "integrity": "sha512-Nzzh/OGxVCOjObjaj1CQF2RUasyYy2Jfuh+zZ3PjLzG2fYRriAiZLib9UKtO+CpQAS3YHiAS+ckZDclwqI1TPA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.10", - "@vitest/utils": "4.1.10", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.0", + "@typescript-eslint/utils": "8.63.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@vitest/spy": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", - "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "node_modules/@typescript-eslint/types": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.63.0.tgz", + "integrity": "sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==", "dev": true, "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, "funding": { - "url": "https://opencollective.com/vitest" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@vitest/utils": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", - "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.63.0.tgz", + "integrity": "sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.10", - "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.1.0" + "@typescript-eslint/project-service": "8.63.0", + "@typescript-eslint/tsconfig-utils": "8.63.0", + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "node_modules/@typescript-eslint/typescript-estree/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": ">=12" + "node": "18 || 20 || >=22" } }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.42", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz", - "integrity": "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" }, "engines": { - "node": ">=6.0.0" + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.63.0.tgz", + "integrity": "sha512-fUKaeAvrTuQg/Tgt3nliAUSZHJM6DlCcfyEmxCvlX8kieWSStBX+5O5Fnidtc3i2JrH+9c/GL4RY2iasd/GPTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.63.0", + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.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.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.63.0.tgz", + "integrity": "sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.63.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/@typescript-eslint/visitor-keys/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/@vitest/coverage-istanbul": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/coverage-istanbul/-/coverage-istanbul-4.1.10.tgz", + "integrity": "sha512-AyNJ5pQRFqCX7pwB9PSTmoVKPaZ4H5IEVJfJsT+q1DYkXvZMEFYgJlyk5sfStmt9rVYRyYYRRsuBeImCOc39ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@istanbuljs/schema": "^0.1.3", + "@jridgewell/gen-mapping": "^0.3.13", + "@jridgewell/trace-mapping": "0.3.31", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "4.1.10" + } + }, + "node_modules/@vitest/coverage-istanbul/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/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "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 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "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.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "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/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "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/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.42", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz", + "integrity": "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/blake3-wasm": { @@ -2169,6 +2757,17 @@ "dev": true, "license": "MIT" }, + "node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/browserslist": { "version": "4.28.5", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.5.tgz", @@ -2203,6 +2802,16 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001802", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001802.tgz", @@ -2234,6 +2843,36 @@ "node": ">=18" } }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/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/cjs-module-lexer": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", @@ -2241,6 +2880,33 @@ "dev": true, "license": "MIT" }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -2262,6 +2928,21 @@ "url": "https://opencollective.com/express" } }, + "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", @@ -2280,6 +2961,13 @@ } } }, + "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/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -2366,6 +3054,176 @@ "node": ">=6" } }, + "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": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.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", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.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", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", + "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "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", @@ -2376,6 +3234,16 @@ "@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.4.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", @@ -2386,6 +3254,27 @@ "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", @@ -2404,6 +3293,57 @@ } } }, + "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/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.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2416,44 +3356,137 @@ "darwin" ], "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.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": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "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/hono": { + "version": "4.12.27", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.27.tgz", + "integrity": "sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "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/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "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": ">=6.9.0" + "node": ">=0.8.19" } }, - "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==", + "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": ">=8" + "node": ">=0.10.0" } }, - "node_modules/hono": { - "version": "4.12.27", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.27.tgz", - "integrity": "sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==", + "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": ">=16.9.0" + "node": ">=0.10.0" } }, - "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==", + "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": "MIT" + "license": "ISC" }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", @@ -2523,6 +3556,29 @@ "dev": true, "license": "MIT" }, + "node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -2536,6 +3592,27 @@ "node": ">=6" } }, + "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/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -2549,6 +3626,16 @@ "node": ">=6" } }, + "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/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -2559,6 +3646,20 @@ "node": ">=6" } }, + "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/lightningcss": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", @@ -2832,6 +3933,29 @@ "url": "https://opencollective.com/parcel" } }, + "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/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -2901,6 +4025,19 @@ "node": ">=22.0.0" } }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2927,6 +4064,13 @@ "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/node-releases": { "version": "2.0.50", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", @@ -2951,6 +4095,89 @@ "node": ">=12.20.0" } }, + "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/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "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/path-to-regexp": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", @@ -3014,6 +4241,52 @@ "node": "^10 || ^12 || >=14" } }, + "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/prettier": { + "version": "3.9.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.5.tgz", + "integrity": "sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "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/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/rolldown": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz", @@ -3106,6 +4379,29 @@ "@img/sharp-win32-x64": "0.34.5" } }, + "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", @@ -3137,6 +4433,19 @@ "dev": true, "license": "MIT" }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/supports-color": { "version": "10.2.2", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", @@ -3194,6 +4503,19 @@ "node": ">=14.0.0" } }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -3202,6 +4524,19 @@ "license": "0BSD", "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", @@ -3216,6 +4551,30 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.63.0.tgz", + "integrity": "sha512-xgwXyzG4sK9ALkBxbyGkTMMOS+imnW65iPhxCQMK83KhxyoDNW7l+IDqEf9vMdoUidHpOoS967RCq4eMiTexwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.63.0", + "@typescript-eslint/parser": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.0", + "@typescript-eslint/utils": "8.63.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.1.0" + } + }, "node_modules/undici": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", @@ -3274,6 +4633,16 @@ "browserslist": ">= 4.21.0" } }, + "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": "8.1.3", "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", @@ -3442,6 +4811,22 @@ } } }, + "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", @@ -3459,6 +4844,16 @@ "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/workerd": { "version": "1.20260625.1", "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260625.1.tgz", @@ -3672,6 +5067,19 @@ "dev": true, "license": "ISC" }, + "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" + } + }, "node_modules/youch": { "version": "4.1.0-beta.10", "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", diff --git a/package.json b/package.json index e1a6cf2..1ee7f28 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,12 @@ "dev": "wrangler dev --env dev", "deploy": "wrangler d1 migrations apply DB --remote && wrangler deploy", "typecheck": "tsc --noEmit", + "lint": "eslint . \".github/scripts/**/*.mjs\"", + "lint:fix": "eslint . \".github/scripts/**/*.mjs\" --fix", + "format": "prettier --write .", + "format:check": "prettier --check .", + "format:all": "npm run format && npm --prefix dashboard run format && npm --prefix .github/actions/report run format", + "format:all:check": "npm run format:check && npm --prefix dashboard run format:check && npm --prefix .github/actions/report run format:check", "test": "vitest run", "test:coverage": "vitest run --coverage", "db:migrate:local": "wrangler d1 migrations apply DB --local --env dev", @@ -19,9 +25,15 @@ }, "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.16.20", + "@eslint/js": "^9.0.0", "@types/node": "^26.0.1", "@vitest/coverage-istanbul": "^4.1.9", + "eslint": "^9.0.0", + "eslint-config-prettier": "^9.1.0", + "globals": "^15.9.0", + "prettier": "^3.3.0", "typescript": "^5.6.3", + "typescript-eslint": "^8.0.0", "vitest": "^4.1.9", "wrangler": "^4.104.0" } diff --git a/src/lib/db.ts b/src/lib/db.ts index 506603a..4e301c7 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -16,16 +16,15 @@ export async function getProjectBySlug(db: D1Database, fullSlug: string): Promis } export async function getProjectById(db: D1Database, id: number): Promise { - const row = await db - .prepare('SELECT * FROM projects WHERE id = ?') - .bind(id) - .first(); + const row = await db.prepare('SELECT * FROM projects WHERE id = ?').bind(id).first(); return row ?? null; } export async function listProjectsWithOwners( db: D1Database, -): Promise> { +): Promise< + Array +> { const { results } = await db .prepare( `SELECT p.*, o.login AS owner_login, o.type AS owner_type, o.avatar_url AS owner_avatar_url @@ -34,7 +33,9 @@ export async function listProjectsWithOwners( ORDER BY o.login, p.repo_name`, ) .all(); - return results as unknown as Array; + return results as unknown as Array< + Project & { owner_login: string; owner_type: string; owner_avatar_url: string | null } + >; } export async function getMetricsTrend( @@ -150,17 +151,11 @@ export async function deleteProjectsByInstallation( db: D1Database, installationId: number, ): Promise { - await db - .prepare('DELETE FROM projects WHERE installation_id = ?') - .bind(installationId) - .run(); + await db.prepare('DELETE FROM projects WHERE installation_id = ?').bind(installationId).run(); } export async function deleteProjectByRepoId(db: D1Database, githubRepoId: number): Promise { - await db - .prepare('DELETE FROM projects WHERE github_repo_id = ?') - .bind(githubRepoId) - .run(); + await db.prepare('DELETE FROM projects WHERE github_repo_id = ?').bind(githubRepoId).run(); } export async function getOwnerByGithubId(db: D1Database, githubId: number): Promise { @@ -264,7 +259,13 @@ export async function getLatestCoverageRun( type LatestCoverage = Pick< CoverageRun, - 'commit_sha' | 'line_coverage' | 'branch_coverage' | 'cyclomatic' | 'cognitive' | 'duplication_pct' | 'maintainability' + | 'commit_sha' + | 'line_coverage' + | 'branch_coverage' + | 'cyclomatic' + | 'cognitive' + | 'duplication_pct' + | 'maintainability' >; /** @@ -469,7 +470,9 @@ async function getOwnLatestTimestamp( if (run) return run.ran_at; const daily = await db - .prepare(`SELECT day FROM coverage_daily WHERE project_id = ?1 AND category = ?2 ORDER BY day DESC LIMIT 1`) + .prepare( + `SELECT day FROM coverage_daily WHERE project_id = ?1 AND category = ?2 ORDER BY day DESC LIMIT 1`, + ) .bind(projectId, category) .first<{ day: string }>(); if (!daily) return null; @@ -686,7 +689,11 @@ export async function getCoverageTrendWindowed( return points; } -async function getProjectCategories(db: D1Database, projectId: number, branch: string): Promise { +async function getProjectCategories( + db: D1Database, + projectId: number, + branch: string, +): Promise { const { results } = await db .prepare( `SELECT DISTINCT category FROM ( @@ -719,7 +726,9 @@ export async function getCoverageTrendGroupedWindowed( let sharedRightEdge: number | undefined; if (align) { - const edges = await Promise.all(categories.map((c) => getOwnLatestTimestamp(db, projectId, branch, c))); + const edges = await Promise.all( + categories.map((c) => getOwnLatestTimestamp(db, projectId, branch, c)), + ); const known = edges.filter((e): e is number => e !== null); if (known.length === 0) return []; sharedRightEdge = Math.max(...known); diff --git a/src/lib/github.ts b/src/lib/github.ts index 44cc599..493b1c7 100644 --- a/src/lib/github.ts +++ b/src/lib/github.ts @@ -29,25 +29,22 @@ export async function getInstallationToken( ): Promise { const appJwt = await mintAppJwt(appId, privateKeyPem); - const res = await fetch( - `${GITHUB_API}/app/installations/${installationId}/access_tokens`, - { - method: 'POST', - headers: { - Authorization: `Bearer ${appJwt}`, - Accept: 'application/vnd.github+json', - 'User-Agent': USER_AGENT, - 'X-GitHub-Api-Version': '2022-11-28', - }, + const res = await fetch(`${GITHUB_API}/app/installations/${installationId}/access_tokens`, { + method: 'POST', + headers: { + Authorization: `Bearer ${appJwt}`, + Accept: 'application/vnd.github+json', + 'User-Agent': USER_AGENT, + 'X-GitHub-Api-Version': '2022-11-28', }, - ); + }); if (!res.ok) { console.error(`Failed to mint installation token: ${res.status}`, await res.text()); throw new Error('Failed to mint installation token'); } - const data = await res.json() as { token: string; expires_at: string }; + const data = (await res.json()) as { token: string; expires_at: string }; return { token: data.token, expiresAt: data.expires_at }; } @@ -69,30 +66,25 @@ export interface GitHubInstallation { } /** Fetch all repos for an installation (handles pagination). */ -export async function fetchInstallationRepos( - installationToken: string, -): Promise { +export async function fetchInstallationRepos(installationToken: string): Promise { const repos: GitHubRepo[] = []; let page = 1; while (true) { - const res = await fetch( - `${GITHUB_API}/installation/repositories?per_page=100&page=${page}`, - { - headers: { - Authorization: `Bearer ${installationToken}`, - Accept: 'application/vnd.github+json', - 'User-Agent': USER_AGENT, - 'X-GitHub-Api-Version': '2022-11-28', - }, + const res = await fetch(`${GITHUB_API}/installation/repositories?per_page=100&page=${page}`, { + headers: { + Authorization: `Bearer ${installationToken}`, + Accept: 'application/vnd.github+json', + 'User-Agent': USER_AGENT, + 'X-GitHub-Api-Version': '2022-11-28', }, - ); + }); if (!res.ok) { throw new Error(`Failed to fetch installation repos: ${res.status}`); } - const data = await res.json() as { repositories: GitHubRepo[]; total_count: number }; + const data = (await res.json()) as { repositories: GitHubRepo[]; total_count: number }; repos.push(...data.repositories); if (repos.length >= data.total_count) break; diff --git a/src/lib/metrics.ts b/src/lib/metrics.ts index c9b8b6d..5d742b0 100644 --- a/src/lib/metrics.ts +++ b/src/lib/metrics.ts @@ -25,7 +25,9 @@ export type CoverageColumn = | 'maintainability'; export function metricToColumn(metric: string): { column: CoverageColumn; unit: string } | null { - return (METRIC_COLUMN_MAP as Record)[metric] ?? null; + return ( + (METRIC_COLUMN_MAP as Record)[metric] ?? null + ); } /** Read the right column value from a typed coverage row. */ diff --git a/src/lib/resync.ts b/src/lib/resync.ts index c1877fe..572c914 100644 --- a/src/lib/resync.ts +++ b/src/lib/resync.ts @@ -33,7 +33,7 @@ export async function performResync(installationId: number, env: Bindings): Prom throw new Error(`Failed to fetch installation: ${installationRes.status}`); } - const installation = await installationRes.json() as { + const installation = (await installationRes.json()) as { account: { id: number; login: string; type: 'User' | 'Organization'; avatar_url: string }; }; diff --git a/src/middleware/access.ts b/src/middleware/access.ts index bbfa60b..332c01e 100644 --- a/src/middleware/access.ts +++ b/src/middleware/access.ts @@ -69,7 +69,13 @@ export function requireAccess() { const header = decodeProtectedHeader(assertion); if (header.alg !== 'RS256') throw new Error(`Unexpected algorithm: ${header.alg}`); - console.log({ event: 'access_verify', path, kid: header.kid, alg: header.alg, teamDomain: CF_ACCESS_TEAM_DOMAIN }); + console.log({ + event: 'access_verify', + path, + kid: header.kid, + alg: header.alg, + teamDomain: CF_ACCESS_TEAM_DOMAIN, + }); let jwks = await fetchAccessJWKS(CF_ACCESS_TEAM_DOMAIN, false); let jwk = jwks.keys.find((k) => k.kid === header.kid); diff --git a/src/middleware/webhook.ts b/src/middleware/webhook.ts index cb7b434..dc18e91 100644 --- a/src/middleware/webhook.ts +++ b/src/middleware/webhook.ts @@ -28,43 +28,43 @@ function hexToBytes(hex: string): Uint8Array { export function requireWebhookHmac() { return createMiddleware<{ Bindings: Bindings; Variables: Variables }>(async (c, next) => { - const sigHeader = c.req.header('X-Hub-Signature-256'); - const deliveryId = c.req.header('X-GitHub-Delivery'); + const sigHeader = c.req.header('X-Hub-Signature-256'); + const deliveryId = c.req.header('X-GitHub-Delivery'); - if (!sigHeader || !deliveryId) { - return c.json({ error: 'Missing webhook headers' }, 400); - } + if (!sigHeader || !deliveryId) { + return c.json({ error: 'Missing webhook headers' }, 400); + } - const rawBody = await c.req.text(); + const rawBody = await c.req.text(); - // Constant-time HMAC verification (A5) - const key = await importHmacKey(c.env.GITHUB_WEBHOOK_SECRET); - const expectedPrefix = 'sha256='; - if (!sigHeader.startsWith(expectedPrefix)) { - return c.json({ error: 'Invalid signature format' }, 400); - } + // Constant-time HMAC verification (A5) + const key = await importHmacKey(c.env.GITHUB_WEBHOOK_SECRET); + const expectedPrefix = 'sha256='; + if (!sigHeader.startsWith(expectedPrefix)) { + return c.json({ error: 'Invalid signature format' }, 400); + } - const sigBytes = hexToBytes(sigHeader.slice(expectedPrefix.length)); - const bodyBytes = new TextEncoder().encode(rawBody); - const valid = await crypto.subtle.verify('HMAC', key, sigBytes, bodyBytes); + const sigBytes = hexToBytes(sigHeader.slice(expectedPrefix.length)); + const bodyBytes = new TextEncoder().encode(rawBody); + const valid = await crypto.subtle.verify('HMAC', key, sigBytes, bodyBytes); - if (!valid) { - return c.json({ error: 'Invalid webhook signature' }, 401); - } + if (!valid) { + return c.json({ error: 'Invalid webhook signature' }, 401); + } - // Replay protection: reject duplicate delivery IDs (A5) - const result = await c.env.DB.prepare( - 'INSERT OR IGNORE INTO webhook_deliveries(delivery_id) VALUES (?)', - ) - .bind(deliveryId) - .run(); + // Replay protection: reject duplicate delivery IDs (A5) + const result = await c.env.DB.prepare( + 'INSERT OR IGNORE INTO webhook_deliveries(delivery_id) VALUES (?)', + ) + .bind(deliveryId) + .run(); - if (result.meta.changes === 0) { - return c.json({ error: 'Duplicate delivery' }, 409); - } + if (result.meta.changes === 0) { + return c.json({ error: 'Duplicate delivery' }, 409); + } - // Make raw body available to route handlers without re-reading the stream - c.set('rawBody', rawBody); - await next(); + // Make raw body available to route handlers without re-reading the stream + c.set('rawBody', rawBody); + await next(); }); } diff --git a/src/routes/api.ts b/src/routes/api.ts index be36e41..4731ac4 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -69,10 +69,19 @@ api.get('/projects/:owner/:repo/metrics/categories', requireAccess(), async (c) let points; if (range !== undefined) { if (!isRangeKey(range)) { - return c.json({ error: 'Validation failed', issues: [{ message: `Unknown range: ${range}` }] }, 422); + return c.json( + { error: 'Validation failed', issues: [{ message: `Unknown range: ${range}` }] }, + 422, + ); } const align = c.req.query('align') === 'true'; - points = await getCoverageTrendGroupedWindowed(c.env.DB, project.id, branch, RANGE_SECONDS[range], align); + points = await getCoverageTrendGroupedWindowed( + c.env.DB, + project.id, + branch, + RANGE_SECONDS[range], + align, + ); } else { const limit = Math.min(Number(c.req.query('limit') ?? '100'), 1000); points = await getCoverageTrendGrouped(c.env.DB, project.id, branch, limit); @@ -80,7 +89,13 @@ api.get('/projects/:owner/:repo/metrics/categories', requireAccess(), async (c) const byCategory = new Map< string, - Array<{ commit_sha: string; value: number; unit: string; recorded_at: string; synthetic?: boolean }> + Array<{ + commit_sha: string; + value: number; + unit: string; + recorded_at: string; + synthetic?: boolean; + }> >(); for (const p of points) { const value = pickColumnValue(p, mapping.column); diff --git a/src/routes/ci.ts b/src/routes/ci.ts index 633010c..b62eb6b 100644 --- a/src/routes/ci.ts +++ b/src/routes/ci.ts @@ -9,7 +9,10 @@ const ci = new Hono<{ Bindings: Bindings; Variables: Variables }>(); const CATEGORY_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i; const CoverageReport = z.object({ - category: z.string().regex(CATEGORY_RE, 'category must be alphanumeric with hyphens/underscores, max 64 chars').optional(), + category: z + .string() + .regex(CATEGORY_RE, 'category must be alphanumeric with hyphens/underscores, max 64 chars') + .optional(), line_coverage: z.number().min(0).max(100), branch_coverage: z.number().min(0).max(100).optional(), cyclomatic: z.number().min(0).optional(), diff --git a/src/routes/webhooks.ts b/src/routes/webhooks.ts index 5838cc5..873cc6b 100644 --- a/src/routes/webhooks.ts +++ b/src/routes/webhooks.ts @@ -1,6 +1,11 @@ import { Hono } from 'hono'; import { requireWebhookHmac } from '../middleware/webhook'; -import { upsertOwner, upsertProject, deleteProjectsByInstallation, deleteProjectByRepoId } from '../lib/db'; +import { + upsertOwner, + upsertProject, + deleteProjectsByInstallation, + deleteProjectByRepoId, +} from '../lib/db'; import { getInstallationToken, fetchRepoMetadata } from '../lib/github'; import type { Bindings, Variables } from '../types'; @@ -57,15 +62,33 @@ async function handleInstallation(payload: Record, env: Binding console.error(`Unexpected account type in installation payload: ${account.type}`); return; } - const ownerId = await upsertOwner(env.DB, account.id, account.login, account.type, account.avatar_url); + const ownerId = await upsertOwner( + env.DB, + account.id, + account.login, + account.type, + account.avatar_url, + ); const repos = (payload.repositories as WebhookRepo[] | undefined) ?? []; - const instToken = await getInstallationToken(env.GITHUB_APP_ID, env.GITHUB_APP_PRIVATE_KEY, installationId); + const instToken = await getInstallationToken( + env.GITHUB_APP_ID, + env.GITHUB_APP_PRIVATE_KEY, + installationId, + ); for (const repo of repos) { // Fetch full metadata (including default_branch) for each repo const meta = await fetchRepoMetadata(instToken.token, repo.full_name); - await upsertProject(env.DB, ownerId, repo.id, repo.name, repo.full_name, installationId, meta.default_branch); + await upsertProject( + env.DB, + ownerId, + repo.id, + repo.name, + repo.full_name, + installationId, + meta.default_branch, + ); } } // Other actions (suspend, unsuspend, new_permissions_accepted) are acknowledged and ignored @@ -82,16 +105,36 @@ async function handleInstallationRepositories( if (action === 'added') { if (account.type !== 'User' && account.type !== 'Organization') { - console.error(`Unexpected account type in installation_repositories payload: ${account.type}`); + console.error( + `Unexpected account type in installation_repositories payload: ${account.type}`, + ); return; } - const ownerId = await upsertOwner(env.DB, account.id, account.login, account.type, account.avatar_url); + const ownerId = await upsertOwner( + env.DB, + account.id, + account.login, + account.type, + account.avatar_url, + ); const added = (payload.repositories_added as WebhookRepo[]) ?? []; - const instToken = await getInstallationToken(env.GITHUB_APP_ID, env.GITHUB_APP_PRIVATE_KEY, installationId); + const instToken = await getInstallationToken( + env.GITHUB_APP_ID, + env.GITHUB_APP_PRIVATE_KEY, + installationId, + ); for (const repo of added) { const meta = await fetchRepoMetadata(instToken.token, repo.full_name); - await upsertProject(env.DB, ownerId, repo.id, repo.name, repo.full_name, installationId, meta.default_branch); + await upsertProject( + env.DB, + ownerId, + repo.id, + repo.name, + repo.full_name, + installationId, + meta.default_branch, + ); } } else if (action === 'removed') { const removed = (payload.repositories_removed as WebhookRepo[]) ?? []; diff --git a/test/admin.test.ts b/test/admin.test.ts index 9025c2f..c920f10 100644 --- a/test/admin.test.ts +++ b/test/admin.test.ts @@ -67,7 +67,9 @@ describe('POST /api/admin/resync', () => { }); it('returns 502 when the resync fails', async () => { - const res = await postJson('/api/admin/resync', { installationId: FAILING_ACCESS_TOKEN_INSTALLATION_ID }); + const res = await postJson('/api/admin/resync', { + installationId: FAILING_ACCESS_TOKEN_INSTALLATION_ID, + }); expect(res.status).toBe(502); }); @@ -78,14 +80,18 @@ describe('POST /api/admin/resync', () => { const owner = await testEnv.DB.prepare('SELECT * FROM owners WHERE github_id = 100').first(); expect(owner).not.toBeNull(); - const repoA = await testEnv.DB.prepare('SELECT * FROM projects WHERE github_repo_id = 1001').first<{ + const repoA = await testEnv.DB.prepare( + 'SELECT * FROM projects WHERE github_repo_id = 1001', + ).first<{ full_slug: string; default_branch: string; }>(); expect(repoA?.full_slug).toBe('fixture-org/repo-a'); expect(repoA?.default_branch).toBe('main'); - const repoB = await testEnv.DB.prepare('SELECT * FROM projects WHERE github_repo_id = 1002').first<{ + const repoB = await testEnv.DB.prepare( + 'SELECT * FROM projects WHERE github_repo_id = 1002', + ).first<{ default_branch: string; }>(); expect(repoB?.default_branch).toBe('develop'); @@ -103,7 +109,9 @@ describe('POST /api/admin/resync', () => { const res = await postJson('/api/admin/resync', { installationId: 102 }); expect(res.status).toBe(200); - const stale = await testEnv.DB.prepare('SELECT * FROM projects WHERE github_repo_id = 99999').first(); + const stale = await testEnv.DB.prepare( + 'SELECT * FROM projects WHERE github_repo_id = 99999', + ).first(); expect(stale).toBeNull(); }); }); @@ -149,7 +157,7 @@ describe('PATCH /api/admin/projects/:id/badge', () => { it('toggles the badge and persists it', async () => { const res = await patchJson('/api/admin/projects/2/badge', { enabled: true }); expect(res.status).toBe(200); - const body = await res.json() as { ok: boolean; badge_enabled: boolean }; + const body = (await res.json()) as { ok: boolean; badge_enabled: boolean }; expect(body.badge_enabled).toBe(true); const row = await testEnv.DB.prepare('SELECT badge_enabled FROM projects WHERE id = 2').first<{ diff --git a/test/api.test.ts b/test/api.test.ts index 204d90f..0b0830a 100644 --- a/test/api.test.ts +++ b/test/api.test.ts @@ -42,7 +42,11 @@ describe('GET /api/projects', () => { const res = await get('/api/projects'); expect(res.status).toBe(200); - const body = await res.json() as Array<{ full_slug: string; owner_login: string; owner_type: string }>; + const body = (await res.json()) as Array<{ + full_slug: string; + owner_login: string; + owner_type: string; + }>; const slugs = body.map((p) => p.full_slug); expect(slugs).toContain('testorg/repo'); @@ -71,11 +75,13 @@ describe('GET /api/projects/:owner/:repo/metrics', () => { `INSERT INTO coverage_runs (project_id, commit_sha, branch, ran_at, line_coverage) VALUES (1, 'sha-1', 'main', ?1, 80), (1, 'sha-2', 'main', ?2, 90)`, - ).bind(now - 86400, now).run(); + ) + .bind(now - 86400, now) + .run(); const res = await get('/api/projects/testorg/repo/metrics'); expect(res.status).toBe(200); - const body = await res.json() as { + const body = (await res.json()) as { project: string; branch: string; metric: string; @@ -93,13 +99,15 @@ describe('GET /api/projects/:owner/:repo/metrics', () => { await testEnv.DB.prepare( `INSERT INTO coverage_runs (project_id, commit_sha, branch, ran_at, line_coverage) VALUES (1, 'sha-a', 'main', ?1, 10), (1, 'sha-b', 'main', ?2, 20)`, - ).bind(now - 86400, now).run(); + ) + .bind(now - 86400, now) + .run(); // Math.min(Number(limit), 1000) — a limit far past the cap must not error or // attempt to over-fetch; it just can't return more rows than exist. const res = await get('/api/projects/testorg/repo/metrics?limit=999999'); expect(res.status).toBe(200); - const body = await res.json() as { data: unknown[] }; + const body = (await res.json()) as { data: unknown[] }; expect(body.data).toHaveLength(2); }); @@ -108,10 +116,12 @@ describe('GET /api/projects/:owner/:repo/metrics', () => { await testEnv.DB.prepare( `INSERT INTO coverage_runs (project_id, commit_sha, branch, ran_at, line_coverage) VALUES (1, 'sha-a', 'main', ?1, 10), (1, 'sha-b', 'main', ?2, 20)`, - ).bind(now - 86400, now).run(); + ) + .bind(now - 86400, now) + .run(); const res = await get('/api/projects/testorg/repo/metrics?limit=1'); - const body = await res.json() as { data: unknown[] }; + const body = (await res.json()) as { data: unknown[] }; expect(body.data).toHaveLength(1); }); @@ -121,10 +131,12 @@ describe('GET /api/projects/:owner/:repo/metrics', () => { `INSERT INTO coverage_runs (project_id, commit_sha, branch, category, ran_at, line_coverage) VALUES (1, 'sha-be', 'main', 'backend', ?1, 90), (1, 'sha-fe', 'main', 'frontend', ?1, 30)`, - ).bind(now).run(); + ) + .bind(now) + .run(); const res = await get('/api/projects/testorg/repo/metrics?category=frontend'); - const body = await res.json() as { data: Array<{ value: number }> }; + const body = (await res.json()) as { data: Array<{ value: number }> }; expect(body.data).toHaveLength(1); expect(body.data[0].value).toBe(30); }); @@ -142,11 +154,13 @@ describe('GET /api/projects/:owner/:repo/metrics/categories', () => { `INSERT INTO coverage_runs (project_id, commit_sha, branch, category, ran_at, line_coverage) VALUES (1, 'sha-be', 'main', 'backend', ?1, 92), (1, 'sha-fe', 'main', 'frontend', ?1, 55)`, - ).bind(now).run(); + ) + .bind(now) + .run(); const res = await get('/api/projects/testorg/repo/metrics/categories'); expect(res.status).toBe(200); - const body = await res.json() as { + const body = (await res.json()) as { categories: Array<{ category: string; data: Array<{ value: number }> }>; }; @@ -165,12 +179,17 @@ describe('GET /api/projects/:owner/:repo/metrics/categories', () => { await testEnv.DB.prepare( `INSERT INTO coverage_runs (project_id, commit_sha, branch, ran_at, line_coverage) VALUES (1, 'sha-only', 'main', ?1, 77)`, - ).bind(now).run(); + ) + .bind(now) + .run(); const res = await get('/api/projects/testorg/repo/metrics/categories?range=15m'); expect(res.status).toBe(200); - const body = await res.json() as { - categories: Array<{ category: string; data: Array<{ value: number; recorded_at: string; synthetic?: boolean }> }>; + const body = (await res.json()) as { + categories: Array<{ + category: string; + data: Array<{ value: number; recorded_at: string; synthetic?: boolean }>; + }>; }; const defaultCat = body.categories.find((c) => c.category === 'default')!; diff --git a/test/badge.test.ts b/test/badge.test.ts index 1c0b3f0..74bbcbe 100644 --- a/test/badge.test.ts +++ b/test/badge.test.ts @@ -29,12 +29,14 @@ async function getBadge( return worker.fetch(new Request(url), testEnv as never); } -async function seedCoverage(fields: Partial<{ - line_coverage: number; - duplication_pct: number; - cyclomatic: number; - category: string; -}>): Promise { +async function seedCoverage( + fields: Partial<{ + line_coverage: number; + duplication_pct: number; + cyclomatic: number; + category: string; + }>, +): Promise { const category = fields.category ?? 'default'; await testEnv.DB.prepare( `INSERT INTO coverage_runs (project_id, commit_sha, branch, category, ran_at, line_coverage, duplication_pct, cyclomatic) @@ -79,7 +81,12 @@ describe('GET /api/badge/:owner/:repo/:metric.json', () => { await seedCoverage({ line_coverage: 87.25 }); const res = await getBadge('testorg', 'repo', 'coverage'); expect(res.status).toBe(200); - const body = await res.json() as { schemaVersion: number; label: string; message: string; color: string }; + const body = (await res.json()) as { + schemaVersion: number; + label: string; + message: string; + color: string; + }; expect(body.schemaVersion).toBe(1); expect(body.label).toBe('coverage'); expect(body.message).toBe('87.3%'); @@ -93,7 +100,7 @@ describe('GET /api/badge/:owner/:repo/:metric.json', () => { ])('coverage badge color at %d%% is %s', async (value, color) => { await seedCoverage({ line_coverage: value }); const res = await getBadge('testorg', 'repo', 'coverage'); - const body = await res.json() as { color: string }; + const body = (await res.json()) as { color: string }; expect(body.color).toBe(color); }); @@ -104,14 +111,14 @@ describe('GET /api/badge/:owner/:repo/:metric.json', () => { ])('duplication badge color at %d%% is %s', async (value, color) => { await seedCoverage({ line_coverage: 50, duplication_pct: value }); const res = await getBadge('testorg', 'repo', 'duplication'); - const body = await res.json() as { color: string }; + const body = (await res.json()) as { color: string }; expect(body.color).toBe(color); }); it('defaults to blue for metrics with no color thresholds', async () => { await seedCoverage({ line_coverage: 50, cyclomatic: 12 }); const res = await getBadge('testorg', 'repo', 'cyclomatic'); - const body = await res.json() as { color: string; message: string }; + const body = (await res.json()) as { color: string; message: string }; expect(body.color).toBe('blue'); expect(body.message).toBe('12'); // unitless metric, no % suffix }); @@ -121,28 +128,28 @@ describe('GET /api/badge/:owner/:repo/:metric.json', () => { await seedCoverage({ line_coverage: 82.5, category: 'backend' }); const res = await getBadge('testorg', 'repo', 'coverage', 'backend'); expect(res.status).toBe(200); - const body = await res.json() as { message: string }; + const body = (await res.json()) as { message: string }; expect(body.message).toBe('82.5%'); }); it('labels the badge with the category name when non-default', async () => { await seedCoverage({ line_coverage: 82.5, category: 'backend' }); const res = await getBadge('testorg', 'repo', 'coverage', 'backend'); - const body = await res.json() as { label: string }; + const body = (await res.json()) as { label: string }; expect(body.label).toBe('backend coverage'); }); it('keeps the plain metric name as the label when category is omitted', async () => { await seedCoverage({ line_coverage: 90 }); const res = await getBadge('testorg', 'repo', 'coverage'); - const body = await res.json() as { label: string }; + const body = (await res.json()) as { label: string }; expect(body.label).toBe('coverage'); }); it('keeps the plain metric name as the label when category is explicitly default', async () => { await seedCoverage({ line_coverage: 90 }); const res = await getBadge('testorg', 'repo', 'coverage', 'default'); - const body = await res.json() as { label: string }; + const body = (await res.json()) as { label: string }; expect(body.label).toBe('coverage'); }); @@ -157,7 +164,7 @@ describe('GET /api/badge/:owner/:repo/:metric.json', () => { await seedCoverage({ line_coverage: 50, cyclomatic: 20, category: 'backend' }); const defaultRes = await getBadge('testorg', 'repo', 'cyclomatic'); const backendRes = await getBadge('testorg', 'repo', 'cyclomatic', 'backend'); - expect((await defaultRes.json() as { message: string }).message).toBe('5'); - expect((await backendRes.json() as { message: string }).message).toBe('20'); + expect(((await defaultRes.json()) as { message: string }).message).toBe('5'); + expect(((await backendRes.json()) as { message: string }).message).toBe('20'); }); }); diff --git a/test/ci.test.ts b/test/ci.test.ts index 26dc452..32441b4 100644 --- a/test/ci.test.ts +++ b/test/ci.test.ts @@ -48,7 +48,7 @@ describe('POST /api/ci/coverage', () => { const token = await signOidcJwt({ ref_type: 'tag', ref: 'refs/tags/v1.0.0' }); const res = await fetchCI({ line_coverage: 90 }, token); expect(res.status).toBe(422); - const body = await res.json() as { error: string }; + const body = (await res.json()) as { error: string }; expect(body.error).toMatch(/branch refs/); }); @@ -62,7 +62,7 @@ describe('POST /api/ci/coverage', () => { const token = await signOidcJwt({ ref: 'refs/heads/feature-x' }); const res = await fetchCI({ line_coverage: 90 }, token); expect(res.status).toBe(422); - const body = await res.json() as { error: string }; + const body = (await res.json()) as { error: string }; expect(body.error).toMatch(/default branch/); }); @@ -76,7 +76,7 @@ describe('POST /api/ci/coverage', () => { const token = await signOidcJwt({}); const res = await fetchCI({ line_coverage: 150 }, token); // out of 0-100 range expect(res.status).toBe(422); - const body = await res.json() as { error: string; issues: unknown[] }; + const body = (await res.json()) as { error: string; issues: unknown[] }; expect(body.error).toBe('Validation failed'); expect(Array.isArray(body.issues)).toBe(true); expect(body.issues.length).toBeGreaterThan(0); @@ -90,10 +90,7 @@ describe('POST /api/ci/coverage', () => { it('accepts a valid report and persists it with 202', async () => { const token = await signOidcJwt({ sha: 'c'.repeat(40) }); - const res = await fetchCI( - { line_coverage: 92.5, branch_coverage: 80, cyclomatic: 4 }, - token, - ); + const res = await fetchCI({ line_coverage: 92.5, branch_coverage: 80, cyclomatic: 4 }, token); expect(res.status).toBe(202); const row = await getLatestCoverageRun(testEnv.DB, 1, 'main'); diff --git a/test/db.test.ts b/test/db.test.ts index 2cc64ff..0170e97 100644 --- a/test/db.test.ts +++ b/test/db.test.ts @@ -241,7 +241,13 @@ describe('getCoverageTrendWindowed — single point, sub-day window', () => { it('flat-lines a lone point across the whole window, anchor pinned left / latest pinned right', async () => { await upsertCoverageRun(testEnv.DB, 1, 'sha-only', 'main', NOW, { line_coverage: 88.4 }); - const points = await getCoverageTrendWindowed(testEnv.DB, 1, 'main', 'default', RANGE_SECONDS['15m']); + const points = await getCoverageTrendWindowed( + testEnv.DB, + 1, + 'main', + 'default', + RANGE_SECONDS['15m'], + ); expect(points).toHaveLength(2); expect(points[0].synthetic).toBe(true); @@ -254,7 +260,13 @@ describe('getCoverageTrendWindowed — single point, sub-day window', () => { it('regression guard: anchor and latest timestamps are distinct, not day-collapsed onto one x', async () => { await upsertCoverageRun(testEnv.DB, 1, 'sha-only', 'main', NOW, { line_coverage: 50 }); - const points = await getCoverageTrendWindowed(testEnv.DB, 1, 'main', 'default', RANGE_SECONDS['15m']); + const points = await getCoverageTrendWindowed( + testEnv.DB, + 1, + 'main', + 'default', + RANGE_SECONDS['15m'], + ); expect(points).toHaveLength(2); const t0 = new Date(points[0].recorded_at).getTime(); @@ -270,9 +282,17 @@ describe('getCoverageTrendWindowed — anchor is the closest prior point, not an await upsertCoverageRun(testEnv.DB, 1, 'sha-latest', 'main', NOW, { line_coverage: 90 }); await upsertCoverageRun(testEnv.DB, 1, 'sha-2d', 'main', NOW - 2 * DAY, { line_coverage: 70 }); await upsertCoverageRun(testEnv.DB, 1, 'sha-5d', 'main', NOW - 5 * DAY, { line_coverage: 50 }); - await upsertCoverageRun(testEnv.DB, 1, 'sha-10d', 'main', NOW - 10 * DAY, { line_coverage: 30 }); + await upsertCoverageRun(testEnv.DB, 1, 'sha-10d', 'main', NOW - 10 * DAY, { + line_coverage: 30, + }); - const points = await getCoverageTrendWindowed(testEnv.DB, 1, 'main', 'default', RANGE_SECONDS['1d']); + const points = await getCoverageTrendWindowed( + testEnv.DB, + 1, + 'main', + 'default', + RANGE_SECONDS['1d'], + ); expect(points[0].synthetic).toBe(true); // Closest prior row (2 days back) wins — not the average of 70/50/30. @@ -288,9 +308,17 @@ describe('getCoverageTrendWindowed — 30d window crosses the raw retention boun await testEnv.DB.prepare( `INSERT INTO coverage_daily (project_id, category, day, line_coverage, run_count) VALUES (1, 'default', ?1, 10, 1)`, - ).bind(oldDay).run(); - - const points = await getCoverageTrendWindowed(testEnv.DB, 1, 'main', 'default', RANGE_SECONDS['30d']); + ) + .bind(oldDay) + .run(); + + const points = await getCoverageTrendWindowed( + testEnv.DB, + 1, + 'main', + 'default', + RANGE_SECONDS['30d'], + ); expect(points[0].synthetic).toBe(true); expect(points[0].line_coverage).toBe(10); @@ -300,13 +328,22 @@ describe('getCoverageTrendWindowed — 30d window crosses the raw retention boun describe('getCoverageTrendGroupedWindowed — align forward-carries stale categories', () => { it('shares one right edge across categories and carries a stale series forward to it', async () => { - await upsertCoverageRun(testEnv.DB, 1, 'sha-be', 'main', NOW, { category: 'backend', line_coverage: 90 }); + await upsertCoverageRun(testEnv.DB, 1, 'sha-be', 'main', NOW, { + category: 'backend', + line_coverage: 90, + }); await upsertCoverageRun(testEnv.DB, 1, 'sha-fe', 'main', NOW - 10 * DAY, { category: 'frontend', line_coverage: 60, }); - const grouped = await getCoverageTrendGroupedWindowed(testEnv.DB, 1, 'main', RANGE_SECONDS['30d'], true); + const grouped = await getCoverageTrendGroupedWindowed( + testEnv.DB, + 1, + 'main', + RANGE_SECONDS['30d'], + true, + ); const backend = grouped.filter((p) => p.category === 'backend'); const frontend = grouped.filter((p) => p.category === 'frontend'); @@ -318,13 +355,22 @@ describe('getCoverageTrendGroupedWindowed — align forward-carries stale catego }); it('without align, categories keep their own independent right edges', async () => { - await upsertCoverageRun(testEnv.DB, 1, 'sha-be', 'main', NOW, { category: 'backend', line_coverage: 90 }); + await upsertCoverageRun(testEnv.DB, 1, 'sha-be', 'main', NOW, { + category: 'backend', + line_coverage: 90, + }); await upsertCoverageRun(testEnv.DB, 1, 'sha-fe', 'main', NOW - 10 * DAY, { category: 'frontend', line_coverage: 60, }); - const grouped = await getCoverageTrendGroupedWindowed(testEnv.DB, 1, 'main', RANGE_SECONDS['30d'], false); + const grouped = await getCoverageTrendGroupedWindowed( + testEnv.DB, + 1, + 'main', + RANGE_SECONDS['30d'], + false, + ); const backend = grouped.filter((p) => p.category === 'backend'); const frontend = grouped.filter((p) => p.category === 'frontend'); diff --git a/test/helpers/outbound.ts b/test/helpers/outbound.ts index 9979068..6312212 100644 --- a/test/helpers/outbound.ts +++ b/test/helpers/outbound.ts @@ -47,7 +47,12 @@ function repos(count: number, prefix: string): FixtureRepo[] { /** installationId -> fixture data for /app/installations/:id and /installation/repositories */ export const FIXTURE_INSTALLATIONS: Record = { 100: { - account: { id: 100, login: 'fixture-org', type: 'Organization', avatar_url: 'https://example.com/a.png' }, + account: { + id: 100, + login: 'fixture-org', + type: 'Organization', + avatar_url: 'https://example.com/a.png', + }, repos: [ { id: 1001, name: 'repo-a', full_name: 'fixture-org/repo-a', default_branch: 'main' }, { id: 1002, name: 'repo-b', full_name: 'fixture-org/repo-b', default_branch: 'develop' }, @@ -55,12 +60,22 @@ export const FIXTURE_INSTALLATIONS: Record = { }, // Pagination: 150 repos spans two pages of 100 + 50 at the hardcoded per_page=100. 101: { - account: { id: 101, login: 'big-org', type: 'Organization', avatar_url: 'https://example.com/b.png' }, + account: { + id: 101, + login: 'big-org', + type: 'Organization', + avatar_url: 'https://example.com/b.png', + }, repos: repos(150, 'repo'), }, // No repos left — exercises performResync's "remove local projects GitHub no longer has" path. 102: { - account: { id: 102, login: 'empty-org', type: 'Organization', avatar_url: 'https://example.com/c.png' }, + account: { + id: 102, + login: 'empty-org', + type: 'Organization', + avatar_url: 'https://example.com/c.png', + }, repos: [], }, }; @@ -79,7 +94,10 @@ function installationIdFromAuth(request: Request): number | null { } function json(body: unknown, status = 200): Response { - return new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } }); + return new Response(JSON.stringify(body), { + status, + headers: { 'Content-Type': 'application/json' }, + }); } /** @@ -102,7 +120,10 @@ export async function githubOutboundService(request: Request, oidcJwk: JWK): Pro if (match && request.method === 'POST') { const id = Number(match[1]); if (id === FAILING_ACCESS_TOKEN_INSTALLATION_ID) return json({ message: 'Server error' }, 500); - return json({ token: `mock-token-${id}`, expires_at: new Date(Date.now() + 600_000).toISOString() }); + return json({ + token: `mock-token-${id}`, + expires_at: new Date(Date.now() + 600_000).toISOString(), + }); } match = url.pathname.match(/^\/app\/installations\/(\d+)$/); diff --git a/test/lib/github.test.ts b/test/lib/github.test.ts index 6397f6f..b86e08b 100644 --- a/test/lib/github.test.ts +++ b/test/lib/github.test.ts @@ -1,18 +1,33 @@ import { describe, it, expect } from 'vitest'; -import { getInstallationToken, fetchInstallationRepos, fetchRepoMetadata } from '../../src/lib/github'; +import { + getInstallationToken, + fetchInstallationRepos, + fetchRepoMetadata, +} from '../../src/lib/github'; import { getTestAppPrivateKeyPem } from '../helpers/crypto'; -import { FAILING_ACCESS_TOKEN_INSTALLATION_ID, NOT_FOUND_REPO_FULL_NAME } from '../helpers/outbound'; +import { + FAILING_ACCESS_TOKEN_INSTALLATION_ID, + NOT_FOUND_REPO_FULL_NAME, +} from '../helpers/outbound'; describe('getInstallationToken', () => { it('mints a token for a valid installation', async () => { - const { token, expiresAt } = await getInstallationToken('test-app-id', getTestAppPrivateKeyPem(), 100); + const { token, expiresAt } = await getInstallationToken( + 'test-app-id', + getTestAppPrivateKeyPem(), + 100, + ); expect(token).toBe('mock-token-100'); expect(new Date(expiresAt).getTime()).toBeGreaterThan(Date.now()); }); it('throws when GitHub fails to mint the token', async () => { await expect( - getInstallationToken('test-app-id', getTestAppPrivateKeyPem(), FAILING_ACCESS_TOKEN_INSTALLATION_ID), + getInstallationToken( + 'test-app-id', + getTestAppPrivateKeyPem(), + FAILING_ACCESS_TOKEN_INSTALLATION_ID, + ), ).rejects.toThrow(/Failed to mint installation token/); }); }); diff --git a/test/lib/resync.test.ts b/test/lib/resync.test.ts index 5c93d1a..a80cc7a 100644 --- a/test/lib/resync.test.ts +++ b/test/lib/resync.test.ts @@ -27,7 +27,9 @@ describe('performResync', () => { await performResync(100, testEnv); - const repoA = await testEnv.DB.prepare('SELECT * FROM projects WHERE github_repo_id = 1001').first<{ + const repoA = await testEnv.DB.prepare( + 'SELECT * FROM projects WHERE github_repo_id = 1001', + ).first<{ full_slug: string; default_branch: string; installation_id: number; @@ -36,7 +38,9 @@ describe('performResync', () => { expect(repoA?.default_branch).toBe('main'); expect(repoA?.installation_id).toBe(100); - const repoB = await testEnv.DB.prepare('SELECT * FROM projects WHERE github_repo_id = 1002').first<{ + const repoB = await testEnv.DB.prepare( + 'SELECT * FROM projects WHERE github_repo_id = 1002', + ).first<{ default_branch: string; }>(); expect(repoB?.default_branch).toBe('develop'); @@ -53,7 +57,9 @@ describe('performResync', () => { await performResync(100, testEnv); - const repoA = await testEnv.DB.prepare('SELECT * FROM projects WHERE github_repo_id = 1001').first<{ + const repoA = await testEnv.DB.prepare( + 'SELECT * FROM projects WHERE github_repo_id = 1001', + ).first<{ full_slug: string; default_branch: string; }>(); @@ -77,7 +83,9 @@ describe('performResync', () => { await performResync(102, testEnv); - const orphan = await testEnv.DB.prepare('SELECT * FROM projects WHERE github_repo_id = 88888').first(); + const orphan = await testEnv.DB.prepare( + 'SELECT * FROM projects WHERE github_repo_id = 88888', + ).first(); expect(orphan).toBeNull(); }); diff --git a/test/middleware/oidc.test.ts b/test/middleware/oidc.test.ts index f943fe8..3c22cbf 100644 --- a/test/middleware/oidc.test.ts +++ b/test/middleware/oidc.test.ts @@ -45,7 +45,8 @@ describe('verifyOidcToken', () => { }); it('rejects a non-RS256 token before any JWKS lookup', async () => { - const toBase64Url = (s: string) => btoa(s).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); + const toBase64Url = (s: string) => + btoa(s).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); const fakeJwt = [ toBase64Url(JSON.stringify({ alg: 'HS256', typ: 'JWT' })), toBase64Url(JSON.stringify({ sub: 'test' })), diff --git a/test/middleware/webhook.test.ts b/test/middleware/webhook.test.ts index 95cf9c5..9b33288 100644 --- a/test/middleware/webhook.test.ts +++ b/test/middleware/webhook.test.ts @@ -26,7 +26,8 @@ async function postWebhook(opts: { if (opts.event !== undefined) headers['X-GitHub-Event'] = opts.event; if (opts.deliveryId !== null) headers['X-GitHub-Delivery'] = opts.deliveryId ?? 'delivery-1'; if (opts.signature !== null) { - headers['X-Hub-Signature-256'] = opts.signature ?? (await signWebhookBody(testEnv.GITHUB_WEBHOOK_SECRET, body)); + headers['X-Hub-Signature-256'] = + opts.signature ?? (await signWebhookBody(testEnv.GITHUB_WEBHOOK_SECRET, body)); } return worker.fetch( new Request('http://localhost/api/webhooks/github', { method: 'POST', headers, body }), @@ -62,7 +63,9 @@ describe('requireWebhookHmac', () => { const res = await postWebhook({ deliveryId: 'd-valid-1' }); expect(res.status).toBe(200); - const row = await testEnv.DB.prepare('SELECT delivery_id FROM webhook_deliveries WHERE delivery_id = ?') + const row = await testEnv.DB.prepare( + 'SELECT delivery_id FROM webhook_deliveries WHERE delivery_id = ?', + ) .bind('d-valid-1') .first(); expect(row).not.toBeNull(); diff --git a/test/rollup.test.ts b/test/rollup.test.ts index 6b9d595..6e59b47 100644 --- a/test/rollup.test.ts +++ b/test/rollup.test.ts @@ -30,7 +30,9 @@ describe('rollupAndPrune', () => { VALUES (1, 'sha-old1', 'main', ?1, 80.0), (1, 'sha-old2', 'main', ?2, 85.0), (1, 'sha-new', 'main', ?3, 90.0)`, - ).bind(old - 86400, old, now).run(); + ) + .bind(old - 86400, old, now) + .run(); await rollupAndPrune(testEnv); @@ -51,7 +53,9 @@ describe('rollupAndPrune', () => { await testEnv.DB.prepare( `INSERT INTO coverage_runs (project_id, commit_sha, branch, ran_at, line_coverage) VALUES (1, 'sha-idem', 'main', ?1, 75.0)`, - ).bind(old).run(); + ) + .bind(old) + .run(); await rollupAndPrune(testEnv); await rollupAndPrune(testEnv); // second call with empty runs — no-op @@ -68,7 +72,9 @@ describe('rollupAndPrune', () => { `INSERT INTO coverage_runs (project_id, commit_sha, branch, category, ran_at, line_coverage) VALUES (1, 'sha-be', 'main', 'backend', ?1, 80.0), (1, 'sha-fe', 'main', 'frontend', ?1, 40.0)`, - ).bind(old).run(); + ) + .bind(old) + .run(); await rollupAndPrune(testEnv); diff --git a/test/routing.test.ts b/test/routing.test.ts index 7ca9a94..4d89e32 100644 --- a/test/routing.test.ts +++ b/test/routing.test.ts @@ -10,7 +10,7 @@ describe('routing', () => { it('GET /api/health returns 200', async () => { const res = await worker.fetch(new Request('http://localhost/api/health'), env as never); expect(res.status).toBe(200); - const body = await res.json() as { status: string }; + const body = (await res.json()) as { status: string }; expect(body.status).toBe('ok'); }); @@ -79,7 +79,7 @@ describe('routing', () => { testEnv as never, ); expect(res.status).toBe(400); - const body = await res.json() as { error: string }; + const body = (await res.json()) as { error: string }; expect(body.error).toBe('Invalid branch'); } finally { delete (testEnv as Record).ENVIRONMENT; diff --git a/test/scheduled.test.ts b/test/scheduled.test.ts index c095cc9..f421819 100644 --- a/test/scheduled.test.ts +++ b/test/scheduled.test.ts @@ -1,5 +1,10 @@ import { describe, it, expect, beforeEach } from 'vitest'; -import { env, createExecutionContext, waitOnExecutionContext, createScheduledController } from 'cloudflare:test'; +import { + env, + createExecutionContext, + waitOnExecutionContext, + createScheduledController, +} from 'cloudflare:test'; import worker from '../src/index'; import type { Bindings } from '../src/types'; @@ -27,7 +32,9 @@ describe('scheduled() cron entry point', () => { `INSERT INTO coverage_runs (project_id, commit_sha, branch, ran_at, line_coverage) VALUES (1, 'sha-old', 'main', ?1, 82.0), (1, 'sha-new', 'main', ?2, 91.0)`, - ).bind(old, now).run(); + ) + .bind(old, now) + .run(); const ctx = createExecutionContext(); const controller = createScheduledController(); @@ -38,7 +45,9 @@ describe('scheduled() cron entry point', () => { // without this, D1 assertions below would race that unawaited promise. await waitOnExecutionContext(ctx); - const remaining = await testEnv.DB.prepare('SELECT commit_sha FROM coverage_runs').all<{ commit_sha: string }>(); + const remaining = await testEnv.DB.prepare('SELECT commit_sha FROM coverage_runs').all<{ + commit_sha: string; + }>(); expect(remaining.results.map((r) => r.commit_sha)).toEqual(['sha-new']); const daily = await testEnv.DB.prepare( diff --git a/test/webhooks.test.ts b/test/webhooks.test.ts index 5db7c29..d1453bc 100644 --- a/test/webhooks.test.ts +++ b/test/webhooks.test.ts @@ -14,7 +14,11 @@ beforeEach(() => { testEnv.GITHUB_APP_PRIVATE_KEY = getTestAppPrivateKeyPem(); }); -async function postWebhookEvent(event: string, payload: unknown, deliveryId: string): Promise { +async function postWebhookEvent( + event: string, + payload: unknown, + deliveryId: string, +): Promise { const body = JSON.stringify(payload); const signature = await signWebhookBody(testEnv.GITHUB_WEBHOOK_SECRET, body); return worker.fetch( @@ -37,11 +41,13 @@ async function getOwnerByGithubId(githubId: number) { } async function getProjectByRepoId(githubRepoId: number) { - return testEnv.DB.prepare('SELECT * FROM projects WHERE github_repo_id = ?').bind(githubRepoId).first<{ - full_slug: string; - default_branch: string; - installation_id: number; - }>(); + return testEnv.DB.prepare('SELECT * FROM projects WHERE github_repo_id = ?') + .bind(githubRepoId) + .first<{ + full_slug: string; + default_branch: string; + installation_id: number; + }>(); } describe('POST /api/webhooks/github', () => { @@ -52,7 +58,12 @@ describe('POST /api/webhooks/github', () => { action: 'created', installation: { id: 42, - account: { id: 555, login: 'neworg', type: 'Organization', avatar_url: 'https://example.com/x.png' }, + account: { + id: 555, + login: 'neworg', + type: 'Organization', + avatar_url: 'https://example.com/x.png', + }, }, repositories: [ { id: 7001, name: 'alpha', full_name: 'neworg/alpha' }, @@ -102,7 +113,12 @@ describe('POST /api/webhooks/github', () => { action: 'added', installation: { id: 43, - account: { id: 556, login: 'addorg', type: 'User', avatar_url: 'https://example.com/y.png' }, + account: { + id: 556, + login: 'addorg', + type: 'User', + avatar_url: 'https://example.com/y.png', + }, }, repositories_added: [{ id: 7101, name: 'gamma', full_name: 'addorg/gamma' }], }, @@ -127,7 +143,10 @@ describe('POST /api/webhooks/github', () => { 'installation_repositories', { action: 'removed', - installation: { id: 44, account: { id: 901, login: 'remorg', type: 'Organization', avatar_url: '' } }, + installation: { + id: 44, + account: { id: 901, login: 'remorg', type: 'Organization', avatar_url: '' }, + }, repositories_removed: [{ id: 8001, name: 'leaving', full_name: 'remorg/leaving' }], }, 'd-repos-removed', @@ -162,7 +181,7 @@ describe('POST /api/webhooks/github', () => { it('acknowledges an unrecognized event with 200 and no side effects', async () => { const res = await postWebhookEvent('ping', { zen: 'test' }, 'd-ping'); expect(res.status).toBe(200); - const body = await res.json() as { ok: boolean }; + const body = (await res.json()) as { ok: boolean }; expect(body.ok).toBe(true); }); @@ -186,7 +205,11 @@ describe('POST /api/webhooks/github', () => { }); it('rejects a replayed delivery id with 409', async () => { - const payload = { action: 'created', installation: { id: 46, account: { id: 559, login: 'x', type: 'User', avatar_url: '' } }, repositories: [] }; + const payload = { + action: 'created', + installation: { id: 46, account: { id: 559, login: 'x', type: 'User', avatar_url: '' } }, + repositories: [], + }; const first = await postWebhookEvent('installation', payload, 'd-replay-wh'); expect(first.status).toBe(200); diff --git a/wrangler.example.json b/wrangler.example.json index 1fb80b3..a9ecb72 100644 --- a/wrangler.example.json +++ b/wrangler.example.json @@ -1,50 +1,50 @@ { - "$schema": "node_modules/wrangler/config-schema.json", - "name": "coverage-tracker", - "main": "src/index.ts", - "send_metrics": false, - "compatibility_date": "2026-06-25", - "compatibility_flags": ["nodejs_compat"], - "assets": { - "directory": "./dashboard/build", - "binding": "ASSETS", - "not_found_handling": "single-page-application", - "run_worker_first": ["/api/*"] - }, - "observability": { "enabled": true }, - "triggers": { "crons": ["30 6 * * *"] }, - "d1_databases": [ - { - "binding": "DB", - "database_name": "coverage", - "migrations_dir": "migrations" - } - ], - "secrets": { - "required": [ - "GITHUB_APP_ID", - "GITHUB_APP_CLIENT_ID", - "GITHUB_APP_PRIVATE_KEY", - "GITHUB_WEBHOOK_SECRET", - "CF_ACCESS_AUD", - "CF_ACCESS_TEAM_DOMAIN" - ] - }, - "build": { - "command": "npm --prefix dashboard run build" - }, - "env": { - "dev": { - "d1_databases": [ - { - "binding": "DB", - "database_name": "coverage", - "migrations_dir": "migrations" - } - ], - "vars": { - "ENVIRONMENT": "development" - } - } - } + "$schema": "node_modules/wrangler/config-schema.json", + "name": "coverage-tracker", + "main": "src/index.ts", + "send_metrics": false, + "compatibility_date": "2026-06-25", + "compatibility_flags": ["nodejs_compat"], + "assets": { + "directory": "./dashboard/build", + "binding": "ASSETS", + "not_found_handling": "single-page-application", + "run_worker_first": ["/api/*"] + }, + "observability": { "enabled": true }, + "triggers": { "crons": ["30 6 * * *"] }, + "d1_databases": [ + { + "binding": "DB", + "database_name": "coverage", + "migrations_dir": "migrations" + } + ], + "secrets": { + "required": [ + "GITHUB_APP_ID", + "GITHUB_APP_CLIENT_ID", + "GITHUB_APP_PRIVATE_KEY", + "GITHUB_WEBHOOK_SECRET", + "CF_ACCESS_AUD", + "CF_ACCESS_TEAM_DOMAIN" + ] + }, + "build": { + "command": "npm --prefix dashboard run build" + }, + "env": { + "dev": { + "d1_databases": [ + { + "binding": "DB", + "database_name": "coverage", + "migrations_dir": "migrations" + } + ], + "vars": { + "ENVIRONMENT": "development" + } + } + } } diff --git a/wrangler.json b/wrangler.json index 6ae4261..a6c1d6a 100644 --- a/wrangler.json +++ b/wrangler.json @@ -1,53 +1,53 @@ { - "$schema": "node_modules/wrangler/config-schema.json", - "name": "demo-coveragetracker-dev", - "main": "src/index.ts", - "send_metrics": false, - "compatibility_date": "2026-06-25", - "compatibility_flags": ["nodejs_compat"], - "assets": { - "directory": "./dashboard/build", - "binding": "ASSETS", - "not_found_handling": "single-page-application", - "run_worker_first": ["/api/*"] - }, - "observability": { "enabled": true }, - "triggers": { "crons": ["30 6 * * *"] }, - "d1_databases": [ - { - "binding": "DB", - "database_name": "coverage-demo", - "migrations_dir": "migrations" - } - ], - "secrets": { - "required": [ - "GITHUB_APP_ID", - "GITHUB_APP_CLIENT_ID", - "GITHUB_APP_PRIVATE_KEY", - "GITHUB_WEBHOOK_SECRET", - "CF_ACCESS_AUD", - "CF_ACCESS_TEAM_DOMAIN" - ] - }, - "build": { - "command": "npm --prefix dashboard clean-install && npm --prefix dashboard run build" - }, - "env": { - "dev": { - "d1_databases": [ - { - "binding": "DB", - "database_name": "coverage", - "migrations_dir": "migrations" - } - ], - "secrets": { - "required": [] - }, - "vars": { - "ENVIRONMENT": "development" - } - } - } + "$schema": "node_modules/wrangler/config-schema.json", + "name": "demo-coveragetracker-dev", + "main": "src/index.ts", + "send_metrics": false, + "compatibility_date": "2026-06-25", + "compatibility_flags": ["nodejs_compat"], + "assets": { + "directory": "./dashboard/build", + "binding": "ASSETS", + "not_found_handling": "single-page-application", + "run_worker_first": ["/api/*"] + }, + "observability": { "enabled": true }, + "triggers": { "crons": ["30 6 * * *"] }, + "d1_databases": [ + { + "binding": "DB", + "database_name": "coverage-demo", + "migrations_dir": "migrations" + } + ], + "secrets": { + "required": [ + "GITHUB_APP_ID", + "GITHUB_APP_CLIENT_ID", + "GITHUB_APP_PRIVATE_KEY", + "GITHUB_WEBHOOK_SECRET", + "CF_ACCESS_AUD", + "CF_ACCESS_TEAM_DOMAIN" + ] + }, + "build": { + "command": "npm --prefix dashboard clean-install && npm --prefix dashboard run build" + }, + "env": { + "dev": { + "d1_databases": [ + { + "binding": "DB", + "database_name": "coverage", + "migrations_dir": "migrations" + } + ], + "secrets": { + "required": [] + }, + "vars": { + "ENVIRONMENT": "development" + } + } + } }