From 3afe82f11356dabd7bd7103216b39952d560927e Mon Sep 17 00:00:00 2001 From: Julius Walton Date: Thu, 4 Jun 2026 01:25:54 -0400 Subject: [PATCH] Add JSDoc type-checking and test coverage gating MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add tsconfig.json + types/globals.d.ts and // @ts-check headers; tsc --checkJs runs clean in strict mode (JSDoc param/cast annotations added where the DOM types needed narrowing). - Add Vitest coverage (Istanbul provider, pool-agnostic) scoped to the pure logic modules, with thresholds (90% statements/lines, 95% functions, 85% branches — branches lower because each UMD wrapper has a browser-only branch unreachable under Node). - Wire `npm run typecheck` and `npm run coverage` into CI; ignore coverage/. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 4 +- .gitignore | 1 + background.js | 6 + checks.js | 5 + constants.js | 1 + content.js | 16 +- package-lock.json | 779 ++++++++++++++++++++++++++++++++++++++- package.json | 6 + popup.js | 10 +- reorder.js | 184 ++++----- tsconfig.json | 24 ++ types/globals.d.ts | 35 ++ vitest.config.mjs | 20 + 13 files changed, 996 insertions(+), 95 deletions(-) create mode 100644 tsconfig.json create mode 100644 types/globals.d.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cef5772..ace7f57 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,6 +37,8 @@ jobs: - run: npm run format:check - - run: npm test + - run: npm run typecheck + + - run: npm run coverage - run: npm run build diff --git a/.gitignore b/.gitignore index 9982174..d98d9e5 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ Thumbs.db *.zip *.xpi node_modules/ +coverage/ diff --git a/background.js b/background.js index 6e86f1b..96226fa 100644 --- a/background.js +++ b/background.js @@ -1,3 +1,4 @@ +// @ts-check // GitHub PR Reverse Comments — background service worker // // Single job: keep the toolbar icon in sync with whether the current tab @@ -19,10 +20,15 @@ const DISABLED_PATH = { 16: "icon-16-disabled.png", 48: "icon-48-disabled.png" } const ACTIVE_URL_RE = /^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+(?:\/commits)?\/?(?:[?#].*)?$/; +/** @param {unknown} url */ function isActiveUrl(url) { return typeof url === "string" && ACTIVE_URL_RE.test(url); } +/** + * @param {number | undefined} tabId + * @param {string | undefined} url + */ function setIconForTab(tabId, url) { const path = isActiveUrl(url) ? ACTIVE_PATH : DISABLED_PATH; chrome.action.setIcon({ tabId, path }).catch(() => { diff --git a/checks.js b/checks.js index 40ab8d0..5fffe6b 100644 --- a/checks.js +++ b/checks.js @@ -1,3 +1,4 @@ +// @ts-check // PR status-checks detection for the Conversation page (issue #1). // // GitHub renders the PR's status checks in the merge box near the BOTTOM @@ -23,6 +24,7 @@ // names are CSS-module-hashed (`MergeBox-module__mergePartialContainer__x`) // so we match on the stable human-readable prefix and fall back through // a couple of related containers. + /** @param {ParentNode} [root] */ function findChecksBox(root) { const scope = root || document; return ( @@ -34,6 +36,7 @@ } // The accessible labels of the individual check rows within the box. + /** @param {ParentNode} [root] */ function getCheckLabels(root) { const scope = root || document; const box = findChecksBox(scope) || scope; @@ -46,8 +49,10 @@ // Precedence: any failure -> failing; else any in-flight -> running; // else any success -> passing; else unknown. Checked in that order so a // single red check dominates the summary, matching GitHub's own rollup. + /** @param {string[] | string} labels */ function deriveChecksState(labels) { const list = Array.isArray(labels) ? labels : [labels || ""]; + /** @param {RegExp} re */ const any = (re) => list.some((l) => re.test(l)); if (any(/(fail|error|timed out|cancel|denied|action required)/i)) { diff --git a/constants.js b/constants.js index 1d4ae7d..f54d504 100644 --- a/constants.js +++ b/constants.js @@ -1,3 +1,4 @@ +// @ts-check // Shared constants for the extension. // // Loaded as a plain (non-module) script before content.js and popup.js. diff --git a/content.js b/content.js index 9b3a0df..3e328e1 100644 --- a/content.js +++ b/content.js @@ -1,3 +1,4 @@ +// @ts-check // GitHub PR Reverse Comments — content script // // Reverses chronological lists on GitHub PR pages so the newest entry @@ -136,7 +137,9 @@ let currentOrder = ORDER.NEWEST; let isSorting = false; + /** @type {MutationObserver[]} */ let observers = []; + /** @type {PrrcTarget[]} */ let activeTargets = []; // [{ el, item, ... }] function getCurrentPageConfig() { @@ -148,6 +151,7 @@ return getCurrentPageConfig() !== null; } + /** @param {string} order */ function applyOrder(order) { const cfg = getCurrentPageConfig(); if (!cfg) return; @@ -246,7 +250,7 @@ } function scrollToChecksBox() { - const box = findChecksBox(); + const box = /** @type {HTMLElement | null} */ (findChecksBox()); if (!box) return; box.scrollIntoView({ behavior: "smooth", block: "center" }); box.style.outline = "2px solid #1f6feb"; @@ -271,7 +275,9 @@ } const state = deriveChecksState(getCheckLabels()); - const indicator = existing || document.createElement("button"); + const indicator = /** @type {HTMLButtonElement} */ ( + existing || document.createElement("button") + ); if (!existing) { indicator.id = CHECKS_STATUS_ID; indicator.type = "button"; @@ -303,6 +309,7 @@ } } + /** @param {HTMLElement} btn */ function updateButtonLabel(btn) { btn.textContent = currentOrder === ORDER.NEWEST ? "↓ Newest first" : "↑ Oldest first"; btn.title = `Click to switch to ${currentOrder === ORDER.NEWEST ? ORDER.OLDEST : ORDER.NEWEST} first`; @@ -310,7 +317,7 @@ chrome.storage.onChanged.addListener((changes, area) => { if (area !== "local" || !changes[STORAGE_KEY]) return; - currentOrder = changes[STORAGE_KEY].newValue || ORDER.NEWEST; + currentOrder = /** @type {string} */ (changes[STORAGE_KEY].newValue || ORDER.NEWEST); const btn = document.getElementById(BUTTON_ID); if (btn) updateButtonLabel(btn); applyOrder(currentOrder); @@ -343,6 +350,7 @@ injectOrUpdateChecksIndicator(); const cfg = getCurrentPageConfig(); + if (!cfg) return; const freshTargets = cfg.getTargets(); if (!freshTargets.length) return; @@ -373,7 +381,7 @@ await chrome.storage.local.set({ [RESET_VERSION_KEY]: CURRENT_RESET_VERSION }); currentOrder = ORDER.NEWEST; } else { - currentOrder = stored[STORAGE_KEY] || ORDER.NEWEST; + currentOrder = /** @type {string} */ (stored[STORAGE_KEY] || ORDER.NEWEST); } startBodyWatcher(); diff --git a/package-lock.json b/package-lock.json index eeb084c..5c68e1b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,22 +1,29 @@ { "name": "github-pr-reverse-comments", - "version": "1.0.6", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "github-pr-reverse-comments", - "version": "1.0.6", + "version": "1.1.0", "license": "MIT", "devDependencies": { "@eslint/js": "^10.0.1", + "@types/chrome": "^0.1.42", + "@types/node": "^25.9.1", + "@vitest/coverage-istanbul": "^4.1.8", "archiver": "^7.0.1", "eslint": "^10.4.1", "eslint-config-prettier": "^10.1.8", "globals": "^17.6.0", "jsdom": "^29.1.1", "prettier": "^3.8.3", + "typescript": "^6.0.3", "vitest": "^4.1.8" + }, + "engines": { + "node": ">=22" } }, "node_modules/@asamuzakjp/css-color": { @@ -70,6 +77,295 @@ "dev": true, "license": "MIT" }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=18" + } + }, "node_modules/@bramus/specificity": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", @@ -487,6 +783,48 @@ "node": ">=12" } }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -494,6 +832,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", @@ -827,6 +1176,17 @@ "assertion-error": "^2.0.1" } }, + "node_modules/@types/chrome": { + "version": "0.1.42", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.1.42.tgz", + "integrity": "sha512-tdT2roFqGecZZDjA9fUEAINb2STxSPifHMDvY6EfRjNRCjdrs/0FwKt5RCIA9MKMd1arAYZZL3nwEkp6ZLZu2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/filesystem": "*", + "@types/har-format": "*" + } + }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -848,6 +1208,30 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/filesystem": { + "version": "0.0.36", + "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.36.tgz", + "integrity": "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/filewriter": "*" + } + }, + "node_modules/@types/filewriter": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.33.tgz", + "integrity": "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/har-format": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.16.tgz", + "integrity": "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==", + "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", @@ -855,6 +1239,74 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "25.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", + "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@vitest/coverage-istanbul": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/coverage-istanbul/-/coverage-istanbul-4.1.8.tgz", + "integrity": "sha512-/h514nMZMKI6foh21mVgO1zlCH6pdDamwKMbla1uLU2GMxTlfp0PQMxovWozmzQdCIQYZ2XXEzg2zNZom2zAOg==", + "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.8" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.8.tgz", + "integrity": "sha512-lt3kovsyHwYe00wq4D1ti0Z974fWj4NLp6siqiyEufUpyFwK9Yhi7rBhac9JL5aA0zoMrJqc4vYPZRUnI7l7nw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.8", + "ast-v8-to-istanbul": "^1.0.0", + "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", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.8", + "vitest": "4.1.8" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { "version": "4.1.8", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz", @@ -1095,6 +1547,20 @@ "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.3.tgz", + "integrity": "sha512-jCMQ6ZylLPudp0CDfBmQBZUsrh1/8psbmu9ibeVWKuHWD0YrH9YABwlKu5kVEFoT0GCQQW9Z/SxfuEbbkGQCRg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -1245,6 +1711,19 @@ ], "license": "MIT" }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.33", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.33.tgz", + "integrity": "sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/bidi-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", @@ -1268,6 +1747,40 @@ "node": "18 || 20 || >=22" } }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -1303,6 +1816,27 @@ "node": ">=8.0.0" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -1483,6 +2017,13 @@ "dev": true, "license": "MIT" }, + "node_modules/electron-to-chromium": { + "version": "1.5.367", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.367.tgz", + "integrity": "sha512-4Mk/mrynCNQ+atY40D3UpmhLWB6AHMbYMlIrPhHcMF6x0L7O0b052FCAsxw1LlaR++UFuNg3D/A6XCuGDa0guQ==", + "dev": true, + "license": "ISC" + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -1510,6 +2051,16 @@ "dev": true, "license": "MIT" }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "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", @@ -1870,6 +2421,16 @@ "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": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", @@ -1958,6 +2519,16 @@ "dev": true, "license": "ISC" }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/html-encoding-sniffer": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", @@ -1971,6 +2542,13 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.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/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -2086,6 +2664,45 @@ "dev": true, "license": "ISC" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", @@ -2102,6 +2719,15 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/jsdom": { "version": "29.1.1", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", @@ -2143,6 +2769,19 @@ } } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -2164,6 +2803,19 @@ "dev": true, "license": "MIT" }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -2538,6 +3190,34 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", + "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mdn-data": { "version": "2.27.1", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", @@ -2604,6 +3284,16 @@ "dev": true, "license": "MIT" }, + "node_modules/node-releases": { + "version": "2.0.47", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", + "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -2983,6 +3673,19 @@ "node": ">=v12.22.7" } }, + "node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -3176,6 +3879,19 @@ "node": ">=8" } }, + "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/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -3327,6 +4043,20 @@ "node": ">= 0.8.0" } }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/undici": { "version": "7.27.0", "resolved": "https://registry.npmjs.org/undici/-/undici-7.27.0.tgz", @@ -3337,6 +4067,44 @@ "node": ">=20.18.1" } }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "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", @@ -3728,6 +4496,13 @@ "dev": true, "license": "MIT" }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "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", diff --git a/package.json b/package.json index c58e4bc..d1106b5 100644 --- a/package.json +++ b/package.json @@ -10,18 +10,24 @@ "lint": "eslint .", "format": "prettier --write .", "format:check": "prettier --check .", + "typecheck": "tsc --noEmit", "test": "vitest run", + "coverage": "vitest run --coverage", "build": "node scripts/build.mjs" }, "license": "MIT", "devDependencies": { "@eslint/js": "^10.0.1", + "@types/chrome": "^0.1.42", + "@types/node": "^25.9.1", + "@vitest/coverage-istanbul": "^4.1.8", "archiver": "^7.0.1", "eslint": "^10.4.1", "eslint-config-prettier": "^10.1.8", "globals": "^17.6.0", "jsdom": "^29.1.1", "prettier": "^3.8.3", + "typescript": "^6.0.3", "vitest": "^4.1.8" } } diff --git a/popup.js b/popup.js index 8bbca2a..d3f7c95 100644 --- a/popup.js +++ b/popup.js @@ -1,3 +1,4 @@ +// @ts-check // STORAGE_KEY and ORDER come from constants.js (loaded first in popup.html). const buttons = { @@ -5,18 +6,19 @@ const buttons = { [ORDER.OLDEST]: document.getElementById("oldest"), }; +/** @param {string} order */ function render(order) { - buttons[ORDER.NEWEST].classList.toggle("active", order === ORDER.NEWEST); - buttons[ORDER.OLDEST].classList.toggle("active", order === ORDER.OLDEST); + buttons[ORDER.NEWEST]?.classList.toggle("active", order === ORDER.NEWEST); + buttons[ORDER.OLDEST]?.classList.toggle("active", order === ORDER.OLDEST); } async function init() { const stored = await chrome.storage.local.get(STORAGE_KEY); - render(stored[STORAGE_KEY] || ORDER.NEWEST); + render(/** @type {string} */ (stored[STORAGE_KEY] || ORDER.NEWEST)); } for (const [order, btn] of Object.entries(buttons)) { - btn.addEventListener("click", () => { + btn?.addEventListener("click", () => { chrome.storage.local.set({ [STORAGE_KEY]: order }); render(order); }); diff --git a/reorder.js b/reorder.js index 26e71de..7fba285 100644 --- a/reorder.js +++ b/reorder.js @@ -1,3 +1,4 @@ +// @ts-check // Pure DOM-reordering helpers, factored out of content.js so they can be // unit-tested under jsdom without pulling in the chrome.* APIs or the // content script's mutation-observer machinery. @@ -11,106 +12,121 @@ const { ORDER } = require("./constants.js"); module.exports = factory(ORDER); } else { - Object.assign(root, factory(root.ORDER)); + Object.assign(root, factory(/** @type {*} */ (root).ORDER)); } -})(typeof globalThis !== "undefined" ? globalThis : this, function (ORDER) { - // Try (container, item) pairs in order. For each, prefer items as - // direct children of the container; fall back to descendant items if - // the container has at least 2 matching descendants. Returns the first - // pair that finds 2+ items, or null. - function firstMatchingTarget(candidates) { - for (const pair of candidates) { - const el = document.querySelector(pair.container); - if (!el) continue; - const direct = el.querySelectorAll(`:scope > ${pair.item}`); - if (direct.length >= 2) { - return { el, item: pair.item, descendant: false }; - } - const any = el.querySelectorAll(pair.item); - if (any.length >= 2) { - return { el, item: pair.item, descendant: true }; +})( + typeof globalThis !== "undefined" ? globalThis : this, + function (/** @type {{ NEWEST: string; OLDEST: string }} */ ORDER) { + // Try (container, item) pairs in order. For each, prefer items as + // direct children of the container; fall back to descendant items if + // the container has at least 2 matching descendants. Returns the first + // pair that finds 2+ items, or null. + /** @param {{ container: string; item: string }[]} candidates */ + function firstMatchingTarget(candidates) { + for (const pair of candidates) { + const el = document.querySelector(pair.container); + if (!el) continue; + const direct = el.querySelectorAll(`:scope > ${pair.item}`); + if (direct.length >= 2) { + return { el, item: pair.item, descendant: false }; + } + const any = el.querySelectorAll(pair.item); + if (any.length >= 2) { + return { el, item: pair.item, descendant: true }; + } } + return null; } - return null; - } - // On the PR Conversation timeline, commits pushed to the PR appear in - // "added N commits" batches. Each batch has a `div[id^="commits-pushed-"]` - // header followed by a sibling wrapper whose .TimelineItem children are - // the individual commits. Returns a reverse-target for every batch - // wrapper holding 2+ commits, so they reorder like any other target. - function pushedCommitTargets(root) { - const scope = root || document; - const targets = []; - for (const header of scope.querySelectorAll('[id^="commits-pushed-"]')) { - const wrapper = header.nextElementSibling; - if (!wrapper) continue; - const commits = Array.from(wrapper.children).filter((c) => c.matches(".TimelineItem")); - if (commits.length >= 2) { - targets.push({ el: wrapper, item: ".TimelineItem", descendant: false }); + // On the PR Conversation timeline, commits pushed to the PR appear in + // "added N commits" batches. Each batch has a `div[id^="commits-pushed-"]` + // header followed by a sibling wrapper whose .TimelineItem children are + // the individual commits. Returns a reverse-target for every batch + // wrapper holding 2+ commits, so they reorder like any other target. + /** @param {ParentNode} [root] */ + function pushedCommitTargets(root) { + const scope = root || document; + /** @type {PrrcTarget[]} */ + const targets = []; + for (const header of scope.querySelectorAll('[id^="commits-pushed-"]')) { + const wrapper = header.nextElementSibling; + if (!wrapper) continue; + const commits = Array.from(wrapper.children).filter((c) => c.matches(".TimelineItem")); + if (commits.length >= 2) { + targets.push({ el: wrapper, item: ".TimelineItem", descendant: false }); + } } + return targets; } - return targets; - } - // Reverse one specific target's items in place. Preserves the slots - // of any non-matching siblings. - function applyOrderToTarget(target, order) { - const { el: container, item, descendant } = target; + // Reverse one specific target's items in place. Preserves the slots + // of any non-matching siblings. + /** + * @param {PrrcTarget} target + * @param {string} order + */ + function applyOrderToTarget(target, order) { + const { el: container, item, descendant } = target; - let itemParent = container; - let items; - if (descendant) { - const firstItem = container.querySelector(item); - itemParent = firstItem?.parentElement || container; - items = Array.from(itemParent.children).filter((c) => c.matches(item)); - } else { - items = Array.from(container.children).filter((c) => c.matches(item)); - } + let itemParent = container; + /** @type {HTMLElement[]} */ + let items; + if (descendant) { + const firstItem = container.querySelector(item); + itemParent = firstItem?.parentElement || container; + items = /** @type {HTMLElement[]} */ ( + Array.from(itemParent.children).filter((c) => c.matches(item)) + ); + } else { + items = /** @type {HTMLElement[]} */ ( + Array.from(container.children).filter((c) => c.matches(item)) + ); + } - if (items.length < 2) return false; + if (items.length < 2) return false; - // Stamp original positions on first sight. - let nextIndex = 0; - for (const it of items) { - const idx = it.dataset.prrcIndex; - if (idx !== undefined) { - const n = parseInt(idx, 10); - if (!Number.isNaN(n) && n >= nextIndex) nextIndex = n + 1; + // Stamp original positions on first sight. + let nextIndex = 0; + for (const it of items) { + const idx = it.dataset.prrcIndex; + if (idx !== undefined) { + const n = parseInt(idx, 10); + if (!Number.isNaN(n) && n >= nextIndex) nextIndex = n + 1; + } } - } - for (const it of items) { - if (it.dataset.prrcIndex === undefined) { - it.dataset.prrcIndex = String(nextIndex++); + for (const it of items) { + if (it.dataset.prrcIndex === undefined) { + it.dataset.prrcIndex = String(nextIndex++); + } } - } - const allChildren = Array.from(itemParent.children); - const slots = allChildren.map((c, i) => (c.matches(item) ? i : -1)).filter((i) => i !== -1); + const allChildren = Array.from(itemParent.children); + const slots = allChildren.map((c, i) => (c.matches(item) ? i : -1)).filter((i) => i !== -1); - const sortedItems = [...items].sort((a, b) => { - const ai = parseInt(a.dataset.prrcIndex, 10); - const bi = parseInt(b.dataset.prrcIndex, 10); - return order === ORDER.NEWEST ? bi - ai : ai - bi; - }); + const sortedItems = [...items].sort((a, b) => { + const ai = parseInt(a.dataset.prrcIndex ?? "", 10); + const bi = parseInt(b.dataset.prrcIndex ?? "", 10); + return order === ORDER.NEWEST ? bi - ai : ai - bi; + }); - const newChildren = allChildren.slice(); - slots.forEach((slotIdx, k) => { - newChildren[slotIdx] = sortedItems[k]; - }); + const newChildren = allChildren.slice(); + slots.forEach((slotIdx, k) => { + newChildren[slotIdx] = sortedItems[k]; + }); - let changed = false; - for (let i = 0; i < newChildren.length; i++) { - if (newChildren[i] !== allChildren[i]) { - changed = true; - break; + let changed = false; + for (let i = 0; i < newChildren.length; i++) { + if (newChildren[i] !== allChildren[i]) { + changed = true; + break; + } } - } - if (!changed) return false; + if (!changed) return false; - for (const c of newChildren) itemParent.appendChild(c); - return true; - } + for (const c of newChildren) itemParent.appendChild(c); + return true; + } - return { firstMatchingTarget, pushedCommitTargets, applyOrderToTarget }; -}); + return { firstMatchingTarget, pushedCommitTargets, applyOrderToTarget }; + }, +); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..3ffc1da --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "nodenext", + "moduleResolution": "nodenext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["chrome", "node"], + "allowJs": true, + "checkJs": true, + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": [ + "constants.js", + "reorder.js", + "checks.js", + "content.js", + "background.js", + "popup.js", + "types/**/*.d.ts" + ] +} diff --git a/types/globals.d.ts b/types/globals.d.ts new file mode 100644 index 0000000..4e4a675 --- /dev/null +++ b/types/globals.d.ts @@ -0,0 +1,35 @@ +// Ambient declarations for the symbols shared across content scripts. +// +// constants.js / reorder.js / checks.js attach these to the global scope +// (the extension injects them into one shared world), and content.js reads +// them as bare globals. Declaring them here lets `tsc --checkJs` verify +// content.js without each file having to import the others. + +interface PrrcTarget { + el: Element; + item: string; + descendant: boolean; +} + +interface PrrcChecksState { + key: string; + label: string; + color: string; +} + +// constants.js — declared with `var` so they also appear on `globalThis` +// (the UMD modules read `globalThis.ORDER`). +declare var STORAGE_KEY: string; +declare var ORDER: { NEWEST: string; OLDEST: string }; + +// reorder.js +declare function firstMatchingTarget( + candidates: { container: string; item: string }[], +): PrrcTarget | null; +declare function pushedCommitTargets(root?: ParentNode): PrrcTarget[]; +declare function applyOrderToTarget(target: PrrcTarget, order: string): boolean; + +// checks.js +declare function findChecksBox(root?: ParentNode): Element | null; +declare function getCheckLabels(root?: ParentNode): string[]; +declare function deriveChecksState(labels: string[] | string): PrrcChecksState; diff --git a/vitest.config.mjs b/vitest.config.mjs index f36da69..0c66d72 100644 --- a/vitest.config.mjs +++ b/vitest.config.mjs @@ -8,5 +8,25 @@ export default defineConfig({ // Threads avoid the process-fork IPC that some sandboxed/CI runners // block; jsdom tests have no need for process isolation. pool: "threads", + coverage: { + // Istanbul (transform-time instrumentation) is pool-agnostic; the v8 + // provider under-reports with the threads pool in some sandboxed/CI + // environments. + provider: "istanbul", + // Measure only the pure, unit-tested logic modules. content.js, + // background.js, and popup.js are DOM/chrome glue exercised in a real + // browser, not under jsdom, so including them would report misleading + // zeros. + include: ["constants.js", "reorder.js", "checks.js"], + reporter: ["text", "lcov"], + // Branches sits a little lower than the rest: each module's UMD + // wrapper has a browser-global branch that can't run under Node tests. + thresholds: { + statements: 90, + branches: 85, + functions: 95, + lines: 90, + }, + }, }, });