From 2b84e52c96b83801eeb26f3e37126c32c1ed7baa Mon Sep 17 00:00:00 2001 From: devhalls Date: Sat, 29 Aug 2026 10:26:24 +0100 Subject: [PATCH 1/2] [UPD] Raise Node floor to 22.12 and take commander and auth majors --- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 2 +- .npmrc | 7 ++--- CONTRIBUTING.md | 2 +- README.md | 4 +-- SKILL.md | 2 +- package-lock.json | 58 ++++++++++++++--------------------- package.json | 6 ++-- scripts/run-tests.mjs | 7 ++--- 9 files changed, 38 insertions(+), 52 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a4a57b4..e35986b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: strategy: fail-fast: false matrix: - node: ["20", "22"] + node: ["22"] steps: - uses: actions/checkout@v7 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 55218d3..88f416f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/setup-node@v7 with: - node-version: "20" + node-version: "22" cache: npm # The tag is the release, and `pend-analytics --version` reads package.json. diff --git a/.npmrc b/.npmrc index a0eade0..23173ae 100644 --- a/.npmrc +++ b/.npmrc @@ -1,7 +1,6 @@ # Treat a dependency's `engines` mismatch as an error, not a warning. # -# Without this, npm prints EBADENGINE and exits 0, so CI happily went green on a -# dependency requiring Node >=22.12 while this package advertised >=20. A -# published CLI cannot claim a runtime it warns on. Failing the install is the -# only version of this check that cannot be overlooked. +# Without this, npm prints EBADENGINE and exits 0. A published CLI cannot +# claim a runtime a dependency refuses. Failing the install is the only +# version of this check that cannot be overlooked. engine-strict=true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cb26b36..a7a036b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,7 +21,7 @@ npm test npm run build ``` -CI runs the same set on Node 20 and 22, plus a full-history secret scan and an +CI runs the same set on Node 22, plus a full-history secret scan and an ASCII punctuation check. ## The rules that matter here diff --git a/README.md b/README.md index 37b33d2..1a686cd 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ test/ tests ## Getting started -Requires **Node.js 20+**. No browser install unless you opt into observeEvents (see docs). +Requires **Node.js 22.12+**. No browser install unless you opt into observeEvents (see docs). Requests to websites use a `pend-analytics/VERSION` user agent so a site owner reading their logs can identify or block us. @@ -199,7 +199,7 @@ Prefer small, single-purpose changes. Start with [Pendulum-url]: https://pendulumdev.co.uk/ [Status-shield]: https://img.shields.io/badge/status-stable-000000?style=for-the-badge [Status-url]: README.md -[Node-shield]: https://img.shields.io/badge/node-20+-000000?style=for-the-badge&logo=nodedotjs +[Node-shield]: https://img.shields.io/badge/node-22.12+-000000?style=for-the-badge&logo=nodedotjs [Node-url]: https://nodejs.org/ [GA4-shield]: https://img.shields.io/badge/GA4-000000?style=for-the-badge&logo=googleanalytics [GA4-url]: https://developers.google.com/analytics diff --git a/SKILL.md b/SKILL.md index 71d2198..3bc8415 100644 --- a/SKILL.md +++ b/SKILL.md @@ -60,5 +60,5 @@ There is no catalog and no checklist. | Pull | `pend-analytics audit -c analytics.toml` | | Validate via MCP | `config_validate` | -Requires Node 20+. A Google service account with read access. Playwright only +Requires Node 22.12+. A Google service account with read access. Playwright only if you opt into observe. diff --git a/package-lock.json b/package-lock.json index 15555b7..80ea2e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,8 @@ "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.30.0", - "commander": "^14.0.3", - "google-auth-library": "^10.9.1", + "commander": "^15.0.0", + "google-auth-library": "^11.0.2", "smol-toml": "^1.7.1", "zod": "^4.4.3" }, @@ -26,7 +26,7 @@ "typescript": "^7.0.2" }, "engines": { - "node": ">=20" + "node": ">=22.12.0" } }, "node_modules/@biomejs/biome": { @@ -98,9 +98,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -118,9 +115,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -138,9 +132,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -158,9 +149,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -1214,12 +1202,12 @@ } }, "node_modules/commander": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", - "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", "license": "MIT", "engines": { - "node": ">=20" + "node": ">=22.12.0" } }, "node_modules/content-disposition": { @@ -1677,17 +1665,17 @@ } }, "node_modules/gcp-metadata": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", - "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-9.0.3.tgz", + "integrity": "sha512-2YYnIlHaKBGT2IPg3G2M57hia9Galz15zsEOvw9T3oRf0lSn6KN6VcHQLqby7x8ksYKnjXvp3rp2KJyLCN6zfQ==", "license": "Apache-2.0", "dependencies": { - "gaxios": "^7.0.0", - "google-logging-utils": "^1.0.0", + "gaxios": "^7.1.3", + "google-logging-utils": "^2.0.0", "json-bigint": "^1.0.0" }, "engines": { - "node": ">=18" + "node": ">=22" } }, "node_modules/get-intrinsic": { @@ -1728,29 +1716,29 @@ } }, "node_modules/google-auth-library": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.9.1.tgz", - "integrity": "sha512-i1ydyHrqcIxXkWh/uBmVkzCvIuq5yiK2ATndIe5XxKholrG/MTYP9xGYka4sQhrbIAgGjL2B6NOE7rFaiF3fXw==", + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-11.0.2.tgz", + "integrity": "sha512-vzpgPutxrghPsnjrjpzLX2bdv8IOL719Rh0oEjGnQu8YCIbnbMuTTQ5zU9LcKvLdOPgCxBwppbvnhgW90Qna5Q==", "license": "Apache-2.0", "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.1.4", - "gcp-metadata": "8.1.2", - "google-logging-utils": "1.1.3", + "gcp-metadata": "^9.0.0", + "google-logging-utils": "^2.0.0", "jws": "^4.0.0" }, "engines": { - "node": ">=18" + "node": ">=22" } }, "node_modules/google-logging-utils": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", - "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-2.0.1.tgz", + "integrity": "sha512-HMhaQghlOTvbcb3c4T5jmmOMtG3JUF1iOQMezaJXL86CDS+Tm2vHd0IeLFRAx3+ewd+bo9E1HFHoy17X5aJa9A==", "license": "Apache-2.0", "engines": { - "node": ">=14" + "node": ">=22" } }, "node_modules/gopd": { diff --git a/package.json b/package.json index 7e7332a..d0ea779 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "NOTICE" ], "engines": { - "node": ">=20" + "node": ">=22.12.0" }, "scripts": { "test": "node scripts/run-tests.mjs", @@ -62,8 +62,8 @@ }, "dependencies": { "@modelcontextprotocol/sdk": "^1.30.0", - "commander": "^14.0.3", - "google-auth-library": "^10.9.1", + "commander": "^15.0.0", + "google-auth-library": "^11.0.2", "smol-toml": "^1.7.1", "zod": "^4.4.3" }, diff --git a/scripts/run-tests.mjs b/scripts/run-tests.mjs index fe1e439..991c665 100644 --- a/scripts/run-tests.mjs +++ b/scripts/run-tests.mjs @@ -6,10 +6,9 @@ * remembers to add never runs, and never fails, so it reads as passing coverage * that does not exist. Discovery removes the human step. * - * Zero dependencies and hand-rolled recursion on purpose. Node's own runner - * cannot do this on our supported floor (Node 20): passing a directory finds no - * `.ts` files, and glob arguments only landed in Node 21. Raising the floor to - * suit the test script would be the tail wagging the dog for a published CLI. + * Zero dependencies and hand-rolled recursion on purpose. Node can glob + * `.ts` files now, but a one-file walker keeps discovery in-repo and + * independent of runner flags. * * node scripts/run-tests.mjs # everything * node scripts/run-tests.mjs score # only paths containing "score" From f38ad846c2767d07db02681dbeb92e93743b6258 Mon Sep 17 00:00:00 2001 From: devhalls Date: Sat, 29 Aug 2026 10:26:24 +0100 Subject: [PATCH 2/2] [FIX] Align Biome schema and format after 2.5.10 --- biome.json | 2 +- src/analytics/rivals.ts | 35 ++++++++++++++++++----------------- test/analytics/config.test.ts | 5 +---- test/analytics/rivals.test.ts | 5 ++++- 4 files changed, 24 insertions(+), 23 deletions(-) diff --git a/biome.json b/biome.json index 2a5c96a..3b236cc 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.5.9/schema.json", + "$schema": "https://biomejs.dev/schemas/2.5.10/schema.json", "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true }, "files": { "includes": ["src/**/*.ts", "test/**/*.ts", "scripts/**/*.{ts,mjs}", "*.json"] diff --git a/src/analytics/rivals.ts b/src/analytics/rivals.ts index f8cccff..e791b53 100644 --- a/src/analytics/rivals.ts +++ b/src/analytics/rivals.ts @@ -1,6 +1,10 @@ -import type { CruxOriginBundle, RivalOriginObservation, RivalPageObservation } from "../types.js"; +import type { + CruxOriginBundle, + RivalOriginObservation, + RivalPageObservation, +} from "../types.js"; import { fetchCruxOrigin, originFromBaseUrl, pagespeedApiKey } from "./crux.js"; -import { fetchText, type FetchLike } from "./tags.js"; +import { type FetchLike, fetchText } from "./tags.js"; const SAMPLE_LIMIT = 5; @@ -11,7 +15,8 @@ function firstMatch(html: string, re: RegExp): string | undefined { } function stripTags(html: string): string { - return html.replace(//gi, " ") + return html + .replace(//gi, " ") .replace(//gi, " ") .replace(/<[^>]+>/g, " ") .replace(/\s+/g, " ") @@ -20,19 +25,18 @@ function stripTags(html: string): string { export function parseRivalPage(html: string, url: string): RivalPageObservation { const title = firstMatch(html, /]*>([\s\S]*?)<\/title>/i); - const description = firstMatch( - html, - /]+name=["']description["'][^>]+content=["']([^"']*)["']/i, - ) ?? firstMatch( - html, - /]+content=["']([^"']*)["'][^>]+name=["']description["']/i, - ); + const description = + firstMatch( + html, + /]+name=["']description["'][^>]+content=["']([^"']*)["']/i, + ) ?? + firstMatch(html, /]+content=["']([^"']*)["'][^>]+name=["']description["']/i); const h1 = firstMatch(html, /]*>([\s\S]*?)<\/h1>/i); const schemaTypes = [ ...new Set( - [...html.matchAll(/"@type"\s*:\s*"([^"]+)"/g)].map((m) => m[1]).filter( - (t): t is string => Boolean(t), - ), + [...html.matchAll(/"@type"\s*:\s*"([^"]+)"/g)] + .map((m) => m[1]) + .filter((t): t is string => Boolean(t)), ), ]; const text = stripTags(html); @@ -49,10 +53,7 @@ export function parseRivalPage(html: string, url: string): RivalPageObservation }; } -async function sitemapSample( - origin: string, - fetchImpl: FetchLike, -): Promise { +async function sitemapSample(origin: string, fetchImpl: FetchLike): Promise { try { const xml = await fetchText(`${origin}/sitemap.xml`, fetchImpl); const locs = [...xml.matchAll(/\s*([^<\s]+)\s*<\/loc>/gi)] diff --git a/test/analytics/config.test.ts b/test/analytics/config.test.ts index b728477..f47a61c 100644 --- a/test/analytics/config.test.ts +++ b/test/analytics/config.test.ts @@ -88,10 +88,7 @@ rivals = ["https://rival.test", "https://peer.test"] siteUrl = "sc-domain:example.com" `); const cfg = loadConfig(path); - assert.deepEqual(cfg.analytics.rivals, [ - "https://rival.test", - "https://peer.test", - ]); + assert.deepEqual(cfg.analytics.rivals, ["https://rival.test", "https://peer.test"]); }); it("rejects enabled analytics without sources", () => { diff --git a/test/analytics/rivals.test.ts b/test/analytics/rivals.test.ts index 78a29ae..c53d997 100644 --- a/test/analytics/rivals.test.ts +++ b/test/analytics/rivals.test.ts @@ -28,7 +28,10 @@ describe("parseRivalPage", () => { }); it("omits fields that were not in the HTML", () => { - const page = parseRivalPage("plain", "https://rival.test/x"); + const page = parseRivalPage( + "plain", + "https://rival.test/x", + ); assert.equal(page.title, undefined); assert.equal(page.description, undefined); assert.equal(page.h1, undefined);