From f87a29b3a01adb418ac237519136254e6fed7d62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Be=C3=B1at=20Espi=C3=B1a?= Date: Fri, 30 Oct 2026 23:33:51 +0100 Subject: [PATCH] ci: hold the page to its Lighthouse budget Nothing protected the Lighthouse scores: a change could take the hero back behind a script or three.js back onto the main thread and CI would stay green. scripts/lighthouse.mjs serves dist/ with Astro's preview server, audits the page on the mobile default and the desktop preset, takes the median of three runs and fails under the budget: 95 in performance, with a little slack for the runner, and 100 in accessibility, best practices and SEO. It prints the scores with LCP, TBT and CLS, names the weighted audits that cost the points, writes the GitHub step summary, and keeps the HTML and JSON reports in lighthouse-reports/. The Cloudflare beacon is blocked during the audit, so an offline run or a third party's outage never counts against the page. CI runs it in its own job, in parallel with Build, so a slow runner cannot hide a type error, and uploads the reports whatever the outcome. Locally, `pnpm build && pnpm lighthouse` does the same; `--runs=1` for a quick look. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/ci.yml | 25 ++ .gitignore | 1 + README.md | 6 +- package.json | 4 +- pnpm-lock.yaml | 924 +++++++++++++++++++++++++++++++++++++++ scripts/lighthouse.mjs | 142 ++++++ 6 files changed, 1099 insertions(+), 3 deletions(-) create mode 100644 scripts/lighthouse.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa145ea..6fae5d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,3 +29,28 @@ jobs: - run: pnpm install --frozen-lockfile - run: pnpm check - run: pnpm build + + # The page's budget: 100 in Lighthouse, with a little slack for the runner. Its own job, so a slow + # runner never hides a type error, and the reports are kept either way. + lighthouse: + name: Lighthouse + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 + - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0 + with: + version: 12.16.0 + - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 + with: + node-version: 24.22.2 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm build + - run: pnpm lighthouse + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + if: always() + with: + name: lighthouse-reports + path: lighthouse-reports/ + retention-days: 14 diff --git a/.gitignore b/.gitignore index 2039125..d9b3d72 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ dist/ node_modules/ .astro/ .DS_Store +lighthouse-reports/ diff --git a/README.md b/README.md index f0ccaf5..ebb8373 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,10 @@ pnpm build # dist/ The 3D scene (`src/scripts/landing/scene.ts`) runs in a Web Worker on an OffscreenCanvas where the browser supports it, and on the page otherwise; `scene-client.ts` decides and `scene-worker.ts` is the worker. Either way the page paints its hero on the first frame and never blocks on three.js, which is -what keeps Lighthouse at 100 on mobile. Measure with `pnpm build && pnpm preview` and Lighthouse -against `http://localhost:4321/`; the Cloudflare beacon is the one request that fails offline. +what keeps Lighthouse at 100 on mobile. `pnpm build && pnpm lighthouse` measures it the way CI does +(`scripts/lighthouse.mjs`: the mobile default and the desktop preset, median of three runs, reports in +`lighthouse-reports/`) and fails under the budget set there: 95 in performance, 100 in the rest. The +Cloudflare beacon is kept out of the audit, so the check works offline and never fails on a third party. pnpm, like the framework repository, and not only for consistency: `npm ci` cannot install this tree on Linux from a lockfile resolved on macOS, because rolldown ships its native bindings as diff --git a/package.json b/package.json index 483b757..acd8c2a 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "dev": "astro dev --host", "build": "astro build", "check": "astro check", - "preview": "astro preview" + "preview": "astro preview", + "lighthouse": "node scripts/lighthouse.mjs" }, "dependencies": { "astro": "7.4.2", @@ -17,6 +18,7 @@ "@astrojs/check": "0.9.12", "@astrojs/sitemap": "3.7.4", "@types/three": "0.186.0", + "lighthouse": "13.6.0", "typescript": "5.9.3", "wrangler": "4.152.0" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c41eb13..7d217f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,6 +24,9 @@ importers: '@types/three': specifier: 0.186.0 version: 0.186.0 + lighthouse: + specifier: 13.6.0 + version: 13.6.0(supports-color@10.2.2) typescript: specifier: 5.9.3 version: 5.9.3 @@ -469,6 +472,21 @@ packages: cpu: [x64] os: [win32] + '@formatjs/ecma402-abstract@2.3.6': + resolution: {integrity: sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==} + + '@formatjs/fast-memoize@2.2.7': + resolution: {integrity: sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==} + + '@formatjs/icu-messageformat-parser@2.11.4': + resolution: {integrity: sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==} + + '@formatjs/icu-skeleton-parser@1.8.16': + resolution: {integrity: sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==} + + '@formatjs/intl-localematcher@0.6.2': + resolution: {integrity: sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==} + '@img/colour@1.1.0': resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} @@ -655,12 +673,144 @@ packages: '@emnapi/core': ^1.7.1 || 2.0.0-beta.2 '@emnapi/runtime': ^1.7.1 || 2.0.0-beta.2 + '@opentelemetry/api@1.10.0': + resolution: {integrity: sha512-lMkyes8iHrG+mk6WH7X70MdCLI2RS0FwNBY1PyQ8UemaijE2m5mv5A0Niy3HtbYTFrgkUEQjFMXmSAk2hQDuPQ==} + engines: {node: '>=8.0.0'} + '@oslojs/encoding@1.1.0': resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} + '@oxc-parser/binding-android-arm-eabi@0.152.0': + resolution: {integrity: sha512-393VtirINkae4XHBTkq/yQxhOwEr4V+r0et37P2fp+sYZSdYgZTRDPo5Um8vBh0QjMZ+RmaMy4PP0qwr4cTMwA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxc-parser/binding-android-arm64@0.152.0': + resolution: {integrity: sha512-iYFcExATJNZ0rZj0o02THbD1Nc9jAW0nVgIQPee6ejbSASpbZXlIMw8jxtpzaG14JQTWwGSZ6U5dEtRJXE6V3Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxc-parser/binding-darwin-arm64@0.152.0': + resolution: {integrity: sha512-fnU+DfUzi5/rFy0LzmipYRz06PAUUkOS8kCJQ3Eg6oqlunC1XZXoffFqxmdL51RmcbYtUG3P8DVG1U5fckoa2A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxc-parser/binding-darwin-x64@0.152.0': + resolution: {integrity: sha512-Y3U2okryWTs7hDld6UhjJwLNf8/do+x4g4C+1U5SiGwrTLwbO83WJYJA8+l9iFSilWBgnfpd/+U76tfeNxUHzA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxc-parser/binding-freebsd-x64@0.152.0': + resolution: {integrity: sha512-XkhkRrrPffEw+ZLv/d/5pEni0buxinm/E/N6fLbJCunD1DBbeCTtVP23ESV+4CWCWdSz397asks1ViGslpmWiQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxc-parser/binding-linux-arm-gnueabihf@0.152.0': + resolution: {integrity: sha512-MLzsRRbdfmdzo6JEhbC1yKU9ItfvM8tEf18HM32jCiNYZEz6J3bRBRl3cTkKR0vBdrk9ZVlcARXtwHccs+epDA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm-musleabihf@0.152.0': + resolution: {integrity: sha512-HHerTEJz7Iy10Ji6BGsk4gqG6PU0UzFuBu91jIEOX89FnrI5ECWSEr7BLpkYQhp7U0QFtMnBhNfQKI0hYZK78w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm64-gnu@0.152.0': + resolution: {integrity: sha512-9Bb8GyzZ2uSLzRDZ27usaKokr07mxiBe2dDF5b6bd2janmaJTT1gL51UfP7AU30yFiXrJvcRyFKREAgKwZYs0w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-arm64-musl@0.152.0': + resolution: {integrity: sha512-om7iDMUroI74A/lfRRWY9/Ev3VWrVsrUZ4tyvyh8raBey/sdcfyAD7tYWc+iykAiotnmftyKTpUKFr1QnaBrAQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-ppc64-gnu@0.152.0': + resolution: {integrity: sha512-Lf1OL2lMTqwDMFC6jnRb30B+WYHKV0XT+bPHcFM6xHtNVwxpobiTzUIvKH6a0LWpvfwyru8Yu6zCGHqqFHH+gg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-gnu@0.152.0': + resolution: {integrity: sha512-BYWSMQK+YJ9ZILStPcmmiSVq8KEG9Qdq/c4BHPC3rUEq/fEDCVjUIwdj5ZzjgZ9P9is9PKu0tcAXzq1ZXg4urQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-musl@0.152.0': + resolution: {integrity: sha512-d2LqHNNNqdH185sRK5dPKWZu201JrNu9/CRFuVEuT+ht7n/QaVblEMWT9MALQPrDWYvhG10P+NSKJFquDWZ09w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-s390x-gnu@0.152.0': + resolution: {integrity: sha512-8gBeHt5rA/8+pKhHl+zLggfa3L/V/L2at96p7ty6pBE7id6lFO19nFUc15u2PcjC4OYeYK465MKy1fhhbpFIGw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-gnu@0.152.0': + resolution: {integrity: sha512-ltgrSF6L+yPTVsIdaAyqKq3zTqzj/1VlNbxWZuxAe8flR3sWdyNsKvdXukoO8/ucJbLickhmieRBge+o/e8Ugw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-musl@0.152.0': + resolution: {integrity: sha512-LcMp6BvrBnewSfdkLHoSqidhoAq4syKOTBV5TYHWd+2VnGkfD/nPRRVIzTBt/Lkbt7pfOFDGtkiTntFS0p6sQQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-openharmony-arm64@0.152.0': + resolution: {integrity: sha512-jbhoMCslVxzRCRNQ8TSHxjYdSKRvk8O6ciLVDkahJvmyHDzBqba/bMQHM6ULKj/rgHNNbSzlIiakdx8SEfkJzA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxc-parser/binding-win32-arm64-msvc@0.152.0': + resolution: {integrity: sha512-JZYVlhv36mr/nertnRB7vchJECOBb1Lz6lltalILsB+o9M/is+H6BEDRLQKdqA4JKs6NjJszo46+iM5sfwj6jQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxc-parser/binding-win32-ia32-msvc@0.152.0': + resolution: {integrity: sha512-1X9oganvVsunRH0lH10UU05N+H9atpX6WsOybByUjwnfcd3h/0tYwvuv16XuHglrIUOnxV2TodbLVEwwafd64g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxc-parser/binding-win32-x64-msvc@0.152.0': + resolution: {integrity: sha512-sw0M80/dn9rcpu+Rqqph/C+MjvDDce6MlXmFjsNQtPEymha0eJy3ml6+foSUTqFfpDIDyHZZ4eoJ43IoMe8bJw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxc-project/types@0.152.0': + resolution: {integrity: sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==} + '@oxc-project/types@0.155.0': resolution: {integrity: sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==} + '@paulirish/trace_engine@0.0.67': + resolution: {integrity: sha512-AWZSKQDwbWKIO//xGvM8isSyDgEDZWDq12SbMQMxl6eE9ZgWaoc9n/2d7REjwcRANyA1Zjo1rVfJiSRFFGKUww==} + '@poppinss/colors@4.1.6': resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} @@ -670,6 +820,19 @@ packages: '@poppinss/exception@1.2.3': resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} + '@puppeteer/browsers@3.2.6': + resolution: {integrity: sha512-MJP29gVoHznmqmwhH1tlZqzfeQ3aylTpSSa6WKZXtkwYUH02i9imBmWGTyZrbnAou5paX78SE4jMAUqnEPf8ug==} + engines: {node: '>=22.12.0'} + hasBin: true + peerDependencies: + proxy-agent: '>=8.0.1' + yauzl: ^2.10.0 || ^3.4.0 + peerDependenciesMeta: + proxy-agent: + optional: true + yauzl: + optional: true + '@rolldown/binding-android-arm-eabi@1.2.15': resolution: {integrity: sha512-aLcgK9gIJ3YRvrrtfSBbRa27F5WrpiTaPulnFpsHPxpUHgFlSgnU4854MHwoRGEBhfRJDWCS2WQ7lIjskE9Olw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -769,6 +932,44 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@sentry/bundler-plugins@11.10.0': + resolution: {integrity: sha512-+ngJ485rIYll5uOhWSvSO+Vx4S24MGe5KcLKlUZghr+BXK+ib32uBOZNG294wGyeauLTDvdjncLEYwiaBnP/5w==} + engines: {node: '>=20.19.0 <22.0.0 || >=22.12.0 <23.0.0 || >=23.2.0'} + peerDependencies: + rollup: '>=3.2.0' + webpack: '>=5.1.0' + peerDependenciesMeta: + rollup: + optional: true + webpack: + optional: true + + '@sentry/conventions@0.29.0': + resolution: {integrity: sha512-kYq9/HJ/l6MYP/srIc20dy1W4mWyJfCF0OWFF+RhBGZwyr/wCmtabRFKyw7kqM+DTwZXQOjSTSZUcJhDowcInQ==} + engines: {node: '>=14'} + + '@sentry/core@11.10.0': + resolution: {integrity: sha512-l/OtTo1v+TQgZtHG2Q5N4IX+gFdAE1tZ27K/P21UCzJglWk54TPEm+d9T2lulPqMK06kZcLwDgRAhE2GO4YS5w==} + engines: {node: '>=20.19.0 <22.0.0 || >=22.12.0 <23.0.0 || >=23.2.0'} + + '@sentry/node@11.10.0': + resolution: {integrity: sha512-ZsndrSn1NjL9AslpUdxz/kYPcv/i+plVZoyPe1ivlr5XPUpocB8pUL5noYbIUl96Wd/yGvPBw6zqO8d69rRKQQ==} + engines: {node: '>=20.19.0 <22.0.0 || >=22.12.0 <23.0.0 || >=23.2.0'} + + '@sentry/opentelemetry@11.10.0': + resolution: {integrity: sha512-YMV4ZgaRlW7QPCcMScVpSBvYOdvDlY+alqRT3KOTcmPKFZtf8O2hXHWFhl/M33kKDEfqOk2XtEVeHs3mZFOCaw==} + engines: {node: '>=20.19.0 <22.0.0 || >=22.12.0 <23.0.0 || >=23.2.0'} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + + '@sentry/server-runtime-injection@11.10.0': + resolution: {integrity: sha512-1nc+HNPldrPEov16sL5m2cOLVgNJ+FsR7QtaWGOOv8qYFJkSp95lyc2qzLnaG2L72UzSMOv10peGRioAKrrZQQ==} + engines: {node: '>=20.19.0 <22.0.0 || >=22.12.0 <23.0.0 || >=23.2.0'} + + '@sentry/server-utils@11.10.0': + resolution: {integrity: sha512-m/MVCvPzQJJZZCTFlVPCrTuoncT7SR80+QKg1K7ul25AsL0byABr5KsHCbXVpOYXLVz04Omk+pOOR5Cv2IAlrA==} + engines: {node: '>=20.19.0 <22.0.0 || >=22.12.0 <23.0.0 || >=23.2.0'} + '@shikijs/core@4.5.0': resolution: {integrity: sha512-6CIgwq+bt2JZ3OSjb/nBkkwEf/CfpHcbam+Mg5Pllco7HAGRQ2/EFhZrrJtbyNTgKwknfVS+llsXGygiDGwJWw==} engines: {node: '>=20'} @@ -898,6 +1099,14 @@ packages: ajv: optional: true + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv-i18n@4.2.0: resolution: {integrity: sha512-v/ei2UkCEeuKNXh8RToiFsUclmU+G57LO1Oo22OagNMENIw+Yb8eMwvHu7Vn9fmkjJyv6XclhJ8TbuigSglPkg==} peerDependencies: @@ -910,6 +1119,14 @@ packages: resolution: {integrity: sha512-KLcsDhqf1XRrJmJr+9I/C0EX2K+sGrAGZFgkmd84c8TJO9zJBu+/kFInjazu1KfJWFhWDKdRWkpBzEs7s35DVA==} hasBin: true + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + ansi-regex@6.4.0: resolution: {integrity: sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==} engines: {node: '>=12'} @@ -939,6 +1156,13 @@ packages: '@astrojs/markdown-remark': optional: true + atomically@2.1.1: + resolution: {integrity: sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ==} + + axe-core@4.14.0: + resolution: {integrity: sha512-9WTZxEjsZ7b13TH8JPmbV2z8CHbl80/2hm3XPEG4JgNdQLK81IBRXmSxHfMAOkSqQeRxT/0dwNDz2GOm3zzpcQ==} + engines: {node: '>=4'} + axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} @@ -946,12 +1170,24 @@ packages: bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + blake3-wasm@2.1.5: resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + brace-expansion@5.0.12: + resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} + engines: {node: 20 || >=22} + + bundle-name@4.1.1: + resolution: {integrity: sha512-DdH81/zPLVS11EUgWq3tEu/xn+EzljlMYooDNdzWEnFha3R3NBMpMV1UqYIpjYHV/SgpFKMIX1Oh7o06SjM/oA==} + engines: {node: '>=18'} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -965,6 +1201,17 @@ packages: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} engines: {node: '>= 20.19.0'} + chrome-launcher@1.2.2: + resolution: {integrity: sha512-sI90u5qf3854bTrWsQMMdsQfSjjMbeSn7QtxVb9zrikf+QgMfHQ71KepHW7ZfixQKrDlAFKb+qnVlSR+xepBzw==} + engines: {node: '>=12.13.0'} + hasBin: true + + chromium-bidi@157.0.8090-0: + resolution: {integrity: sha512-KJw7IQgqXwxpJgJlEIPg5mr2ZoqA+jkTRtc5EABI7sUJ3bu3cpI7NuEBoj4Gn1AsF78CaT9So5WQWspK3/wYag==} + engines: {node: '>=20.19.0 <22.0.0 || >=22.12.0'} + peerDependencies: + devtools-protocol: '*' + ci-info@4.4.0: resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} engines: {node: '>=8'} @@ -988,6 +1235,10 @@ packages: resolution: {integrity: sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng==} engines: {node: '>= 18'} + configstore@8.0.0: + resolution: {integrity: sha512-U51WPZg+o6FDFhBCV6yYFEspGMCoHvCgJSGFw9AwsW2u75gYBPoDlYca5XfDs4TdMUu3/y0R6FFdvvgyH31mKQ==} + engines: {node: '>=20'} + cookie-es@1.2.3: resolution: {integrity: sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==} @@ -1002,6 +1253,9 @@ packages: crossws@0.3.5: resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} + csp_evaluator@1.1.8: + resolution: {integrity: sha512-EwOnfYuNbTytvbMKsLixTrRgnjOa0WZCxGy8A9nnSYAicrdwn+T/epU/yjgymmOxlgKnvH+8wXt+7p/8ak5Feg==} + css-select@6.0.0: resolution: {integrity: sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw==} @@ -1021,6 +1275,30 @@ packages: resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.5.1: + resolution: {integrity: sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==} + engines: {node: '>=18'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + defu@6.1.7: resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} @@ -1042,6 +1320,12 @@ packages: devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + devtools-protocol@0.0.1704951: + resolution: {integrity: sha512-GQhQOs9u/Mvm1zMtJIH872JS4AskuZdUSsPNPk4ChahtWAUqgz6atEtIsMt+nO7MqF1Ic4HxII/k8Cu36JivNg==} + + devtools-protocol@0.0.1727723: + resolution: {integrity: sha512-laxmpDN+zItbpy5klVbHqBsZ38O2rC6VwI9W5daM11Pc3Dj2gruc2wwnHW48CB8wYiWHALDkA6vRg92U2cfdLA==} + diff@9.0.0: resolution: {integrity: sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==} engines: {node: '>=0.3.1'} @@ -1059,6 +1343,14 @@ packages: domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + dot-prop@10.2.0: + resolution: {integrity: sha512-BTJ9aZYL3vCfZlZOBLy9v8TUqWGQ0pzFnygKwFZt5udj6viBoFIBviKPUoZLDCPn1FoXffv6McQFDenrm5Krfw==} + engines: {node: '>=20'} + + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} + engines: {node: '>=12'} + dset@3.1.4: resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==} engines: {node: '>=4'} @@ -1069,6 +1361,10 @@ packages: emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + enquirer@2.4.1: + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} + engines: {node: '>=8.6'} + entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -1088,6 +1384,10 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + eventemitter3@5.0.4: resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} @@ -1155,9 +1455,20 @@ packages: github-slugger@2.0.0: resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + h3@1.15.11: resolution: {integrity: sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==} + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + hast-util-to-html@9.0.5: resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} @@ -1173,18 +1484,66 @@ packages: http-cache-semantics@4.3.0: resolution: {integrity: sha512-M5t5LlJpS1UHMjvwRQVdFHvPISGeLAxNcrWuJkeGh0KxsqCHZ1O3NXZU/8x7cD0BDcGW8kapxMKTvwlqrNkHkA==} + http-link-header@1.1.4: + resolution: {integrity: sha512-xT3GPW6/ZbGuw4UvwHqErSCEjNUlwbQJuZn9/q5U4WEKfp2kENVCAlousG1zLxHeaQ/ffOHUNpWamvkbBW0eNw==} + engines: {node: '>=6.0.0'} + + image-ssim@0.2.0: + resolution: {integrity: sha512-W7+sO6/yhxy83L0G7xR8YAc5Z5QFtYEXXRV6EaE8tuYBZJnA3gVgp3q7X7muhLZVodeb9UfvjSbwt9VJwjIYAg==} + + intl-messageformat@10.7.18: + resolution: {integrity: sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==} + iron-webcrypto@1.2.1: resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + is-docker@4.0.0: resolution: {integrity: sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA==} engines: {node: '>=20'} hasBin: true + is-in-ssh@1.0.0: + resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} + engines: {node: '>=20'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + is-plain-obj@4.1.0: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-safe-filename@0.1.1: + resolution: {integrity: sha512-4SrR7AdnY11LHfDKTZY1u6Ga3RuxZdl3YKWWShO5iyuG5h8QS4GD2tOb04peBJ5I7pXbR+CGBNEhTcwK+FzN3g==} + engines: {node: '>=20'} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} + + jpeg-js@0.4.4: + resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==} + + js-library-detector@6.7.0: + resolution: {integrity: sha512-c80Qupofp43y4cJ7+8TTDN/AsDwLi5oOm/plBrWI+iQt485vKXCco+yVmOwEgdo9VOdsYTuV0UlTeetVPTriXA==} + engines: {node: '>=12'} + json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} @@ -1198,6 +1557,17 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} + lighthouse-logger@2.0.2: + resolution: {integrity: sha512-vWl2+u5jgOQuZR55Z1WM0XDdrJT6mzMP8zHUct7xTlWhuQs+eV0g+QL0RQdFjT54zVmbhLCP8vIVpy1wGn/gCg==} + + lighthouse-stack-packs@1.12.3: + resolution: {integrity: sha512-d8IsOpE83kbANgnM+Tp8+x6HcMpX9o2ITBiUERssgzAIFdZCQzs/f4k6D0DLQTE59enml9mbAOU52Wu35exWtg==} + + lighthouse@13.6.0: + resolution: {integrity: sha512-2/HUPUpQyGm01VEgYCUjSlNZY7Zkhk1v3QI4c7UTbnaoRB7HF0qYMPzzaiidNf5iqw5wnmtm9LpGawKKqCfLmg==} + engines: {node: '>=22.19'} + hasBin: true + lightningcss-android-arm64@1.33.0: resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} engines: {node: '>= 12.0.0'} @@ -1282,6 +1652,9 @@ packages: magicast@0.5.5: resolution: {integrity: sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==} + marky@1.3.0: + resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==} + mdast-util-to-hast@13.2.1: resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} @@ -1313,10 +1686,28 @@ packages: resolution: {integrity: sha512-QNgCA/axizZ7kv7RV3nwbRoz+w4watBlwIsyXjwgYHw4eSw5BrR1F2152s3Kz+Bzy/YxjKFHrZl3LzBRbPgYoQ==} engines: {node: '>=22.0.0'} + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + modern-tar@0.8.6: + resolution: {integrity: sha512-LYSks0tNVqbtez/9OB0YFP6qLRSqkdAxJWOQpVaR67lNTbj2zNLyYpowkRMia5lXLCZKzxwoxkxH2MbbBR2aEg==} + engines: {node: '>=18.0.0'} + mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} @@ -1358,6 +1749,14 @@ packages: oniguruma-to-es@4.3.6: resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==} + open@11.0.4: + resolution: {integrity: sha512-++Zlftm0kVLPmzC06t6epuWmcRMDbI4z5P3NNX979WA/k23+NtSOynEGzsVfZwguKw2mi5umVgnBlJQMwRz4Pg==} + engines: {node: '>=20'} + + oxc-parser@0.152.0: + resolution: {integrity: sha512-ZztsZgaCWmPeKYMdoTT5e0oV/oxD1NeLWNR3kAtr/G707cUPYaaozRlVDHLCGnszHJRszN9mJwmPNpqgwloC0A==} + engines: {node: ^20.19.0 || >=22.12.0} + p-limit@7.3.3: resolution: {integrity: sha512-SKgVvDU5TNIxQ4r30U75BNLC6a+6GOEjngrk/ysM5+Zu3oJuk1SC5ApzIzY/7PjCEjoVPMWJ0zho/4QUiEB7TQ==} engines: {node: '>=20'} @@ -1376,6 +1775,10 @@ packages: path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} @@ -1400,6 +1803,10 @@ packages: resolution: {integrity: sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==} engines: {node: ^10 || ^12 || >=14} + powershell-utils@0.2.1: + resolution: {integrity: sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A==} + engines: {node: '>=20'} + prettier@3.9.10: resolution: {integrity: sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==} engines: {node: '>=14'} @@ -1416,6 +1823,10 @@ packages: property-information@7.2.0: resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} + puppeteer-core@25.16.0: + resolution: {integrity: sha512-VA5Pf54N8shfopNkZZXCdiiwfnnY8ZJYAMqFQRhlH5mGUhfwf53Ar770GPtdpTDLUqvQeYpIBtC9/G2Jij2IJw==} + engines: {node: '>=22.12.0'} + radix3@1.1.2: resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} @@ -1448,11 +1859,19 @@ packages: retext-smartypants@6.2.0: resolution: {integrity: sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==} + robots-parser@3.0.1: + resolution: {integrity: sha512-s+pyvQeIKIZ0dx5iJiQk1tPLJAWln39+MI5jtM8wnyws+G5azk+dMnMX0qfbqNetKKNgcWWOdi0sfm+FbQbgdQ==} + engines: {node: '>=10.0.0'} + rolldown@1.2.15: resolution: {integrity: sha512-6odND+fel5zVw3I4Yc2e69gvguDnX0CAAGiNDZSq4ik5WUcaLrkD2GLuog0XcODbor31hZ5ayEMgVDno+KPV5g==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + satteri@0.10.5: resolution: {integrity: sha512-Ao1LKpAEa9Wdg0otgbVKViZHEq9ebdXe4DMrp3s9vQAU0HNIuHnFEuMuOcm0ZIXyV0Yzxj91NvhLpvXZJO/5ZQ==} @@ -1465,6 +1884,11 @@ packages: engines: {node: '>=10'} hasBin: true + sentry@0.45.0: + resolution: {integrity: sha512-kgmEbLlC22lL7uneeUm9Sjz65rp5cvwYOMTxXbZCmBum4sbVre/lLT3zTV3kwXHJ9/BM8ZS0/GhZe9Cl1t6WyA==} + engines: {node: '>=20.0'} + hasBin: true + sharp@0.35.5: resolution: {integrity: sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==} engines: {node: '>=20.9.0'} @@ -1497,6 +1921,10 @@ packages: space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + speedline-core@1.4.3: + resolution: {integrity: sha512-DI7/OuAUD+GMpR6dmu8lliO2Wg5zfeh+/xsdyJZCzd8o5JgFUjCeLsBDuZjIQJdwXS3J0L/uZYrELKYqx+PXog==} + engines: {node: '>=8.0'} + std-env@4.3.0: resolution: {integrity: sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==} @@ -1511,19 +1939,40 @@ packages: stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + strip-ansi@7.2.0: resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} engines: {node: '>=12'} + stubborn-fs@2.0.0: + resolution: {integrity: sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==} + + stubborn-utils@1.0.2: + resolution: {integrity: sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==} + supports-color@10.2.2: resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} engines: {node: '>=18'} + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + svgo@4.1.0: resolution: {integrity: sha512-bkxnTg1kSU0guhIBmibA6UUhrQmPVA1XsQLN+ylCd+UWzbnLkySOcXpyk1mrl05f+pcaCx2eHb+sp6BgMZWX+Q==} engines: {node: '>=16'} hasBin: true + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + + third-party-web@0.30.0: + resolution: {integrity: sha512-p+PfyL5U0ediGzvwPzMSdD9MnC8rHW6nWJJVLIQvqN7RsSqfaZ+7n4c9GILqH36EYR0eXB9CzAeCRnKFr/9uJg==} + three@0.186.1: resolution: {integrity: sha512-blFeqb49wRCSGUGj7gtpfnSGHy2lwDk94RhUmS1c/hTby70kvChbWpkJ4Pm1390LqzzvTmzgXKHPEafJwCb8jA==} @@ -1545,6 +1994,12 @@ packages: tinyps@0.5.1: resolution: {integrity: sha512-B6Ykif5HflC3AdDkS4Zst/2muYVfeXoVMy+KorqH80s/1x4vunIORpNySH681X7fjj+dFM9MDKD6/Hsccjb87Q==} + tldts-core@7.4.20: + resolution: {integrity: sha512-C630TWXZCZmLq0F2bW4QqwnaaxCQ/svpAqvJwnC4JvgEM5ohhBnGsEzTLSOXcN+xjz+kEaHS5uoWIw2klnqd0A==} + + tldts-icann@7.4.19: + resolution: {integrity: sha512-lnGUR5BZm5UpNDYMBFGAr9pxS2RTUr/euXuJ9o1irR+zKhsBAFUn7quHoZt8UP/eIwykp4ye5f56bCjMBytbTg==} + trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -1554,6 +2009,13 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + type-fest@5.10.0: + resolution: {integrity: sha512-NoSdpq/WEiAg5sjmBkmV/hfxv6HJH4NqPNrqjtSO5CwRmpsDfaf4begxW34KdJykH/l1yHtwBWQkCRdoXO8mPA==} + engines: {node: '>=20'} + + typed-query-selector@2.12.3: + resolution: {integrity: sha512-/dtuMZyutHLlrxWhhC1uib00ELUr2dZ8C9B8YzZYsJmeE3V56OxRblrSM2UaF142pcMtak7ehZ6+poWGeehNIg==} + typesafe-path@0.2.2: resolution: {integrity: sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA==} @@ -1840,6 +2302,12 @@ packages: vscode-uri@3.2.0: resolution: {integrity: sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==} + webdriver-bidi-protocol@1.0.1: + resolution: {integrity: sha512-WXWs/aXF2a6Al1Z2/V/7Gp2oPZOAAmHjJOTmv4KCXRJ122IMj3tSSMc6a4q3K4QOmhQbse/Ahr+oTVJOU2dUiw==} + + when-exit@2.1.5: + resolution: {integrity: sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==} + workerd@1.20261019.1: resolution: {integrity: sha512-UUYRUq/7DmeCl11YriNbcVbjUZVLtt8uuCXFu+nz77hGFf8d4DEVXoCYtnA+MlBatE4+wxoj47DVqzK3xGBAiw==} engines: {node: '>=16'} @@ -1871,6 +2339,26 @@ packages: utf-8-validate: optional: true + ws@8.22.0: + resolution: {integrity: sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + wsl-utils@1.0.1: + resolution: {integrity: sha512-c16PKLIRxBYpR3jSWsdS7SFp0COm5SOQdiFjY818dYFHJbMGuLBszpF07LBWmOS1uzrHa9VGdynsEgoeKy0oOQ==} + engines: {node: '>=20'} + + xdg-basedir@5.1.0: + resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==} + engines: {node: '>=12'} + xxhash-wasm@1.1.0: resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==} @@ -2270,6 +2758,32 @@ snapshots: '@esbuild/win32-x64@0.28.2': optional: true + '@formatjs/ecma402-abstract@2.3.6': + dependencies: + '@formatjs/fast-memoize': 2.2.7 + '@formatjs/intl-localematcher': 0.6.2 + decimal.js: 10.6.0 + tslib: 2.8.1 + + '@formatjs/fast-memoize@2.2.7': + dependencies: + tslib: 2.8.1 + + '@formatjs/icu-messageformat-parser@2.11.4': + dependencies: + '@formatjs/ecma402-abstract': 2.3.6 + '@formatjs/icu-skeleton-parser': 1.8.16 + tslib: 2.8.1 + + '@formatjs/icu-skeleton-parser@1.8.16': + dependencies: + '@formatjs/ecma402-abstract': 2.3.6 + tslib: 2.8.1 + + '@formatjs/intl-localematcher@0.6.2': + dependencies: + tslib: 2.8.1 + '@img/colour@1.1.0': {} '@img/sharp-darwin-arm64@0.35.5': @@ -2405,10 +2919,75 @@ snapshots: '@tybys/wasm-util': 0.10.4 optional: true + '@opentelemetry/api@1.10.0': {} + '@oslojs/encoding@1.1.0': {} + '@oxc-parser/binding-android-arm-eabi@0.152.0': + optional: true + + '@oxc-parser/binding-android-arm64@0.152.0': + optional: true + + '@oxc-parser/binding-darwin-arm64@0.152.0': + optional: true + + '@oxc-parser/binding-darwin-x64@0.152.0': + optional: true + + '@oxc-parser/binding-freebsd-x64@0.152.0': + optional: true + + '@oxc-parser/binding-linux-arm-gnueabihf@0.152.0': + optional: true + + '@oxc-parser/binding-linux-arm-musleabihf@0.152.0': + optional: true + + '@oxc-parser/binding-linux-arm64-gnu@0.152.0': + optional: true + + '@oxc-parser/binding-linux-arm64-musl@0.152.0': + optional: true + + '@oxc-parser/binding-linux-ppc64-gnu@0.152.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-gnu@0.152.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-musl@0.152.0': + optional: true + + '@oxc-parser/binding-linux-s390x-gnu@0.152.0': + optional: true + + '@oxc-parser/binding-linux-x64-gnu@0.152.0': + optional: true + + '@oxc-parser/binding-linux-x64-musl@0.152.0': + optional: true + + '@oxc-parser/binding-openharmony-arm64@0.152.0': + optional: true + + '@oxc-parser/binding-win32-arm64-msvc@0.152.0': + optional: true + + '@oxc-parser/binding-win32-ia32-msvc@0.152.0': + optional: true + + '@oxc-parser/binding-win32-x64-msvc@0.152.0': + optional: true + + '@oxc-project/types@0.152.0': {} + '@oxc-project/types@0.155.0': {} + '@paulirish/trace_engine@0.0.67': + dependencies: + third-party-web: 0.30.0 + '@poppinss/colors@4.1.6': dependencies: kleur: 4.1.5 @@ -2421,6 +3000,11 @@ snapshots: '@poppinss/exception@1.2.3': {} + '@puppeteer/browsers@3.2.6': + dependencies: + modern-tar: 0.8.6 + yargs: 18.2.0 + '@rolldown/binding-android-arm-eabi@1.2.15': optional: true @@ -2468,6 +3052,52 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} + '@sentry/bundler-plugins@11.10.0': + dependencies: + '@sentry/core': 11.10.0 + dotenv: 17.4.2 + glob: 13.0.6 + magic-string: 1.4.3 + oxc-parser: 0.152.0 + sentry: 0.45.0 + supports-color: 8.1.1 + + '@sentry/conventions@0.29.0': {} + + '@sentry/core@11.10.0': + dependencies: + '@sentry/conventions': 0.29.0 + + '@sentry/node@11.10.0': + dependencies: + '@opentelemetry/api': 1.10.0 + '@sentry/bundler-plugins': 11.10.0 + '@sentry/conventions': 0.29.0 + '@sentry/core': 11.10.0 + '@sentry/opentelemetry': 11.10.0(@opentelemetry/api@1.10.0) + '@sentry/server-runtime-injection': 11.10.0 + '@sentry/server-utils': 11.10.0 + transitivePeerDependencies: + - rollup + - webpack + + '@sentry/opentelemetry@11.10.0(@opentelemetry/api@1.10.0)': + dependencies: + '@opentelemetry/api': 1.10.0 + '@sentry/conventions': 0.29.0 + '@sentry/core': 11.10.0 + + '@sentry/server-runtime-injection@11.10.0': + dependencies: + '@sentry/core': 11.10.0 + '@sentry/server-utils': 11.10.0 + + '@sentry/server-utils@11.10.0': + dependencies: + '@opentelemetry/api': 1.10.0 + '@sentry/conventions': 0.29.0 + '@sentry/core': 11.10.0 + '@shikijs/core@4.5.0': dependencies: '@shikijs/primitive': 4.5.0 @@ -2622,6 +3252,10 @@ snapshots: optionalDependencies: ajv: 8.20.0 + ajv-formats@3.0.1: + dependencies: + ajv: 8.20.0 + ajv-i18n@4.2.0(ajv@8.20.0): dependencies: ajv: 8.20.0 @@ -2637,6 +3271,10 @@ snapshots: dependencies: process-ancestry: 0.1.0 + ansi-colors@4.1.3: {} + + ansi-regex@5.0.1: {} + ansi-regex@6.4.0: {} ansi-styles@6.2.3: {} @@ -2739,14 +3377,31 @@ snapshots: - uploadthing - yaml + atomically@2.1.1: + dependencies: + stubborn-fs: 2.0.0 + when-exit: 2.1.5 + + axe-core@4.14.0: {} + axobject-query@4.1.0: {} bail@2.0.2: {} + balanced-match@4.0.4: {} + blake3-wasm@2.1.5: {} boolbase@1.0.0: {} + brace-expansion@5.0.12: + dependencies: + balanced-match: 4.0.4 + + bundle-name@4.1.1: + dependencies: + run-applescript: 7.1.0 + ccount@2.0.1: {} character-entities-html4@2.1.0: {} @@ -2757,6 +3412,21 @@ snapshots: dependencies: readdirp: 5.1.1 + chrome-launcher@1.2.2(supports-color@10.2.2): + dependencies: + '@types/node': 24.19.2 + escape-string-regexp: 4.0.0 + is-wsl: 2.2.0 + lighthouse-logger: 2.0.2(supports-color@10.2.2) + transitivePeerDependencies: + - supports-color + + chromium-bidi@157.0.8090-0(devtools-protocol@0.0.1704951): + dependencies: + devtools-protocol: 0.0.1704951 + mitt: 3.0.1 + zod: 4.6.7 + ci-info@4.4.0: {} cliui@9.0.1: @@ -2773,6 +3443,14 @@ snapshots: common-ancestor-path@2.0.0: {} + configstore@8.0.0: + dependencies: + atomically: 2.1.1 + dot-prop: 10.2.0 + graceful-fs: 4.2.11 + is-safe-filename: 0.1.1 + xdg-basedir: 5.1.0 + cookie-es@1.2.3: {} cookie@1.1.1: {} @@ -2783,6 +3461,8 @@ snapshots: dependencies: uncrypto: 0.1.3 + csp_evaluator@1.1.8: {} + css-select@6.0.0: dependencies: boolbase: 1.0.0 @@ -2807,6 +3487,23 @@ snapshots: dependencies: css-tree: 2.2.1 + debug@4.4.3(supports-color@10.2.2): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 10.2.2 + + decimal.js@10.6.0: {} + + default-browser-id@5.0.1: {} + + default-browser@5.5.1: + dependencies: + bundle-name: 4.1.1 + default-browser-id: 5.0.1 + + define-lazy-prop@3.0.0: {} + defu@6.1.7: {} dequal@2.0.3: {} @@ -2821,6 +3518,10 @@ snapshots: dependencies: dequal: 2.0.3 + devtools-protocol@0.0.1704951: {} + + devtools-protocol@0.0.1727723: {} + diff@9.0.0: {} dom-serializer@2.0.0: @@ -2841,6 +3542,12 @@ snapshots: domelementtype: 2.3.0 domhandler: 5.0.3 + dot-prop@10.2.0: + dependencies: + type-fest: 5.10.0 + + dotenv@17.4.2: {} + dset@3.1.4: {} emmet@2.4.11: @@ -2850,6 +3557,11 @@ snapshots: emoji-regex@10.6.0: {} + enquirer@2.4.1: + dependencies: + ansi-colors: 4.1.3 + strip-ansi: 6.0.1 + entities@4.5.0: {} error-stack-parser-es@1.0.5: {} @@ -2887,6 +3599,8 @@ snapshots: escalade@3.2.0: {} + escape-string-regexp@4.0.0: {} + eventemitter3@5.0.4: {} extend@3.0.2: {} @@ -2936,6 +3650,14 @@ snapshots: github-slugger@2.0.0: {} + glob@13.0.6: + dependencies: + minimatch: 10.2.6 + minipass: 7.1.3 + path-scurry: 2.0.2 + + graceful-fs@4.2.11: {} + h3@1.15.11: dependencies: cookie-es: 1.2.3 @@ -2948,6 +3670,8 @@ snapshots: ufo: 1.6.4 uncrypto: 0.1.3 + has-flag@4.0.0: {} + hast-util-to-html@9.0.5: dependencies: '@types/hast': 3.0.5 @@ -2972,12 +3696,47 @@ snapshots: http-cache-semantics@4.3.0: {} + http-link-header@1.1.4: {} + + image-ssim@0.2.0: {} + + intl-messageformat@10.7.18: + dependencies: + '@formatjs/ecma402-abstract': 2.3.6 + '@formatjs/fast-memoize': 2.2.7 + '@formatjs/icu-messageformat-parser': 2.11.4 + tslib: 2.8.1 + iron-webcrypto@1.2.1: {} + is-docker@2.2.1: {} + + is-docker@3.0.0: {} + is-docker@4.0.0: {} + is-in-ssh@1.0.0: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + is-plain-obj@4.1.0: {} + is-safe-filename@0.1.1: {} + + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + + jpeg-js@0.4.4: {} + + js-library-detector@6.7.0: {} + json-schema-traverse@1.0.0: {} jsonc-parser@2.3.1: {} @@ -2986,6 +3745,48 @@ snapshots: kleur@4.1.5: {} + lighthouse-logger@2.0.2(supports-color@10.2.2): + dependencies: + debug: 4.4.3(supports-color@10.2.2) + marky: 1.3.0 + transitivePeerDependencies: + - supports-color + + lighthouse-stack-packs@1.12.3: {} + + lighthouse@13.6.0(supports-color@10.2.2): + dependencies: + '@paulirish/trace_engine': 0.0.67 + '@sentry/node': 11.10.0 + ajv-formats: 3.0.1 + axe-core: 4.14.0 + chrome-launcher: 1.2.2(supports-color@10.2.2) + configstore: 8.0.0 + csp_evaluator: 1.1.8 + devtools-protocol: 0.0.1727723 + enquirer: 2.4.1 + http-link-header: 1.1.4 + intl-messageformat: 10.7.18 + js-library-detector: 6.7.0 + lighthouse-logger: 2.0.2(supports-color@10.2.2) + lighthouse-stack-packs: 1.12.3 + open: 11.0.4 + puppeteer-core: 25.16.0 + robots-parser: 3.0.1 + speedline-core: 1.4.3 + third-party-web: 0.30.0 + tldts-icann: 7.4.19 + yargs: 18.2.0 + yargs-parser: 22.0.0 + transitivePeerDependencies: + - bufferutil + - proxy-agent + - rollup + - supports-color + - utf-8-validate + - webpack + - yauzl + lightningcss-android-arm64@1.33.0: optional: true @@ -3047,6 +3848,8 @@ snapshots: '@babel/types': 7.29.11 source-map-js: 1.2.2 + marky@1.3.0: {} + mdast-util-to-hast@13.2.1: dependencies: '@types/hast': 3.0.5 @@ -3095,8 +3898,20 @@ snapshots: - bufferutil - utf-8-validate + minimatch@10.2.6: + dependencies: + brace-expansion: 5.0.12 + + minipass@7.1.3: {} + + mitt@3.0.1: {} + + modern-tar@0.8.6: {} + mrmime@2.0.1: {} + ms@2.1.3: {} + muggle-string@0.4.1: {} nanoid@3.3.20: {} @@ -3133,6 +3948,39 @@ snapshots: regex: 6.1.0 regex-recursion: 6.0.2 + open@11.0.4: + dependencies: + default-browser: 5.5.1 + define-lazy-prop: 3.0.0 + is-in-ssh: 1.0.0 + is-inside-container: 1.0.0 + powershell-utils: 0.2.1 + wsl-utils: 1.0.1 + + oxc-parser@0.152.0: + dependencies: + '@oxc-project/types': 0.152.0 + optionalDependencies: + '@oxc-parser/binding-android-arm-eabi': 0.152.0 + '@oxc-parser/binding-android-arm64': 0.152.0 + '@oxc-parser/binding-darwin-arm64': 0.152.0 + '@oxc-parser/binding-darwin-x64': 0.152.0 + '@oxc-parser/binding-freebsd-x64': 0.152.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.152.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.152.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.152.0 + '@oxc-parser/binding-linux-arm64-musl': 0.152.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.152.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.152.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.152.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.152.0 + '@oxc-parser/binding-linux-x64-gnu': 0.152.0 + '@oxc-parser/binding-linux-x64-musl': 0.152.0 + '@oxc-parser/binding-openharmony-arm64': 0.152.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.152.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.152.0 + '@oxc-parser/binding-win32-x64-msvc': 0.152.0 + p-limit@7.3.3: dependencies: yocto-queue: 1.2.2 @@ -3148,6 +3996,11 @@ snapshots: path-browserify@1.0.1: {} + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.3 + minipass: 7.1.3 + path-to-regexp@6.3.0: {} pathe@2.0.3: {} @@ -3166,6 +4019,8 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.2 + powershell-utils@0.2.1: {} + prettier@3.9.10: {} prismjs@1.30.0: {} @@ -3174,6 +4029,20 @@ snapshots: property-information@7.2.0: {} + puppeteer-core@25.16.0: + dependencies: + '@puppeteer/browsers': 3.2.6 + chromium-bidi: 157.0.8090-0(devtools-protocol@0.0.1704951) + devtools-protocol: 0.0.1704951 + typed-query-selector: 2.12.3 + webdriver-bidi-protocol: 1.0.1 + ws: 8.22.0 + transitivePeerDependencies: + - bufferutil + - proxy-agent + - utf-8-validate + - yauzl + radix3@1.1.2: {} readdirp@5.1.1: {} @@ -3202,6 +4071,8 @@ snapshots: nlcst-to-string: 4.0.0 unist-util-visit: 5.2.0 + robots-parser@3.0.1: {} + rolldown@1.2.15: dependencies: '@oxc-project/types': 0.155.0 @@ -3223,6 +4094,8 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.2.15 '@rolldown/binding-win32-x64-msvc': 1.2.15 + run-applescript@7.1.0: {} + satteri@0.10.5: dependencies: '@types/estree-jsx': 1.0.5 @@ -3244,6 +4117,8 @@ snapshots: semver@7.8.5: {} + sentry@0.45.0: {} + sharp@0.35.5(@types/node@24.19.2): dependencies: '@img/colour': 1.1.0 @@ -3303,6 +4178,12 @@ snapshots: space-separated-tokens@2.0.2: {} + speedline-core@1.4.3: + dependencies: + '@types/node': 24.19.2 + image-ssim: 0.2.0 + jpeg-js: 0.4.4 + std-env@4.3.0: {} string-width@7.2.0: @@ -3321,12 +4202,26 @@ snapshots: character-entities-html4: 2.1.0 character-entities-legacy: 3.0.0 + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + strip-ansi@7.2.0: dependencies: ansi-regex: 6.4.0 + stubborn-fs@2.0.0: + dependencies: + stubborn-utils: 1.0.2 + + stubborn-utils@1.0.2: {} + supports-color@10.2.2: {} + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + svgo@4.1.0: dependencies: commander: 11.1.0 @@ -3337,6 +4232,10 @@ snapshots: picocolors: 1.1.1 sax: 1.6.1 + tagged-tag@1.0.0: {} + + third-party-web@0.30.0: {} + three@0.186.1: {} tiny-inflate@1.0.3: {} @@ -3352,12 +4251,24 @@ snapshots: tinyps@0.5.1: {} + tldts-core@7.4.20: {} + + tldts-icann@7.4.19: + dependencies: + tldts-core: 7.4.20 + trim-lines@3.0.1: {} trough@2.2.0: {} tslib@2.8.1: {} + type-fest@5.10.0: + dependencies: + tagged-tag: 1.0.0 + + typed-query-selector@2.12.3: {} + typesafe-path@0.2.2: {} typescript-auto-import-cache@0.3.6: @@ -3570,6 +4481,10 @@ snapshots: vscode-uri@3.2.0: {} + webdriver-bidi-protocol@1.0.1: {} + + when-exit@2.1.5: {} + workerd@1.20261019.1: optionalDependencies: '@cloudflare/workerd-darwin-64': 1.20261019.1 @@ -3603,6 +4518,15 @@ snapshots: ws@8.21.0: {} + ws@8.22.0: {} + + wsl-utils@1.0.1: + dependencies: + is-wsl: 3.1.1 + powershell-utils: 0.2.1 + + xdg-basedir@5.1.0: {} + xxhash-wasm@1.1.0: {} y18n@5.0.8: {} diff --git a/scripts/lighthouse.mjs b/scripts/lighthouse.mjs new file mode 100644 index 0000000..2c0cf3c --- /dev/null +++ b/scripts/lighthouse.mjs @@ -0,0 +1,142 @@ +// Lighthouse against the built site, the way CI runs it: serves dist/ with Astro's preview server, +// audits the page on the mobile default and the desktop preset, takes the median of a few runs, and +// fails when a category falls under its budget. Reports land in lighthouse-reports/. +// +// pnpm build && pnpm lighthouse # three runs per preset, as in CI +// pnpm lighthouse --runs=1 # a quick look +// pnpm lighthouse --out=somewhere --url=/ # where the reports go, and which page + +import { spawn } from "node:child_process"; +import { appendFileSync, existsSync, mkdirSync, readFileSync, rmSync } from "node:fs"; +import { createRequire } from "node:module"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { preview } from "astro"; + +/** A category's lowest acceptable score. Performance has a little slack for the runner; the rest is exact. */ +const BUDGET = { performance: 0.95, accessibility: 1, "best-practices": 1, seo: 1 }; +/** Third parties the page loads but does not control, kept out of the audit so that neither their + * outages nor an offline run count against it (Lighthouse ignores the console errors of what it blocks). */ +const BLOCKED = ["https://static.cloudflareinsights.com/*"]; +const PRESETS = [ + { name: "mobile", flags: [] }, + { name: "desktop", flags: ["--preset=desktop"] }, +]; + +const root = fileURLToPath(new URL("..", import.meta.url)); +const args = Object.fromEntries( + process.argv.slice(2).map((arg) => { + const [key, value = "true"] = arg.replace(/^--/, "").split("="); + return [key, value]; + }), +); +const runs = Math.max(1, Number(args.runs ?? 3) || 3); +const out = path.resolve(root, args.out ?? "lighthouse-reports"); +const page = args.url ?? "/"; + +if (!existsSync(path.join(root, "dist", "index.html"))) { + console.error("dist/index.html is missing: run `pnpm build` first."); + process.exit(2); +} +rmSync(out, { recursive: true, force: true }); +mkdirSync(out, { recursive: true }); + +const lighthouseCli = createRequire(import.meta.url).resolve("lighthouse/cli/index.js"); + +// The preview server lives in this process, so Lighthouse has to run without blocking the event loop. +// Its own log is kept quiet and shown only when a run fails. +const lighthouse = (args) => + new Promise((resolve, reject) => { + const child = spawn(process.execPath, [lighthouseCli, ...args], { stdio: ["ignore", "ignore", "pipe"] }); + let log = ""; + child.stderr.on("data", (chunk) => { + log += chunk; + }); + child.on("error", reject); + child.on("close", (code) => { + if (code === 0) resolve(); + else reject(new Error(`lighthouse exited with ${code}\n${log.split("\n").slice(-30).join("\n")}`)); + }); + }); + +const audit = async (url, name, flags) => { + const file = path.join(out, name); + await lighthouse([ + url, + "--output=json", + "--output=html", + `--output-path=${file}`, + `--only-categories=${Object.keys(BUDGET).join(",")}`, + ...BLOCKED.map((pattern) => `--blocked-url-patterns=${pattern}`), + "--chrome-flags=--headless=new --no-sandbox", + ...flags, + ]); + const report = JSON.parse(readFileSync(`${file}.report.json`, "utf8")); + const scores = Object.fromEntries( + Object.entries(report.categories).map(([id, category]) => [id, category.score ?? 0]), + ); + const metrics = Object.fromEntries( + ["first-contentful-paint", "largest-contentful-paint", "total-blocking-time", "cumulative-layout-shift"].map( + (id) => [id, report.audits[id]?.displayValue ?? "–"], + ), + ); + // The audits that cost each category points: the weighted ones under a perfect score. + const failing = Object.fromEntries( + Object.entries(report.categories).map(([id, category]) => [ + id, + category.auditRefs + .filter((ref) => ref.weight > 0 && (report.audits[ref.id]?.score ?? 1) < 1) + .map((ref) => ref.id), + ]), + ); + return { scores, metrics, failing }; +}; + +const median = (values) => { + const sorted = [...values].sort((a, b) => a - b); + const middle = Math.floor(sorted.length / 2); + return sorted.length % 2 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2; +}; +const percent = (score) => String(Math.round(score * 100)).padStart(3); + +process.env.ASTRO_TELEMETRY_DISABLED = "1"; +const server = await preview({ root, logLevel: "error", server: { host: "127.0.0.1", port: 4321 } }); +const url = new URL(page, `http://127.0.0.1:${server.port}`).href; + +let failed = false; +const lines = [`| Preset | ${Object.keys(BUDGET).join(" | ")} | LCP | TBT | CLS |`, `|---|${"---|".repeat(Object.keys(BUDGET).length + 3)}`]; +try { + for (const preset of PRESETS) { + const results = []; + for (let i = 1; i <= runs; i++) { + process.stderr.write(`lighthouse: ${preset.name} ${i}/${runs} on ${url}\n`); + results.push(await audit(url, `${preset.name}-${i}`, preset.flags)); + } + // The run whose performance score is the median stands for the preset, so its metrics match its scores. + const scores = Object.fromEntries( + Object.keys(BUDGET).map((id) => [id, median(results.map((r) => r.scores[id]))]), + ); + const representative = + results.find((r) => r.scores.performance === scores.performance) ?? results[0]; + const under = Object.entries(BUDGET).filter(([id, floor]) => scores[id] < floor); + if (under.length) failed = true; + const cells = Object.keys(BUDGET).map((id) => `${percent(scores[id])}${scores[id] < BUDGET[id] ? " ✗" : ""}`); + const m = representative.metrics; + lines.push( + `| ${preset.name} | ${cells.join(" | ")} | ${m["largest-contentful-paint"]} | ${m["total-blocking-time"]} | ${m["cumulative-layout-shift"]} |`, + ); + for (const [id, floor] of under) { + const culprits = [...new Set(results.flatMap((r) => r.failing[id] ?? []))].join(", ") || "none"; + console.error(`${preset.name}: ${id} ${percent(scores[id])} is under its budget of ${percent(floor)}. Failing audits: ${culprits}`); + } + } +} finally { + await server.stop(); +} + +const table = lines.join("\n"); +console.log(`\n${table}\n\nReports: ${path.relative(root, out)}/`); +if (process.env.GITHUB_STEP_SUMMARY) { + appendFileSync(process.env.GITHUB_STEP_SUMMARY, `## Lighthouse (median of ${runs})\n\n${table}\n`); +} +process.exit(failed ? 1 : 0);