From ce868d426ab5ca5eb792e02303d8f0fba68fdffe Mon Sep 17 00:00:00 2001 From: ustaxs Date: Thu, 27 Aug 2026 08:56:11 +0100 Subject: [PATCH 1/6] feat: wire up Lighthouse CI and fix web-vitals v5 type errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace deprecated FID (First Input Delay) with INP (Interaction to Next Paint) in PerformanceMetric enum and webVitals.ts to match web-vitals v5 API - Fix 7 type errors in webVitals.ts: proper MetricType usage, remove stale onFID import, add RatedMetric type for type-safe threshold lookups - Add @lhci/cli to devDependencies, remove from depcheck ignore list - Update lighthouserc.json with desktop preset, performance/accessibility/ best-practices/seo category budgets, and per-metric numeric assertions - Add Lighthouse CI job to bundle-size.yml running against the expo web export - Update PERFORMANCE_MONITORING.md to reflect INP replacing FID ๐Ÿค– Generated with Codebuff Co-Authored-By: Codebuff --- .github/workflows/bundle-size.yml | 5 + docs/PERFORMANCE_MONITORING.md | 4 +- lighthouserc.json | 24 +- package-lock.json | 4342 ++++++++++++++++++++++++++++- package.json | 4 +- src/services/webVitals.ts | 58 +- src/utils/trackingEvents.ts | 4 +- 7 files changed, 4297 insertions(+), 144 deletions(-) diff --git a/.github/workflows/bundle-size.yml b/.github/workflows/bundle-size.yml index e9ef9b44..fe16e782 100644 --- a/.github/workflows/bundle-size.yml +++ b/.github/workflows/bundle-size.yml @@ -30,6 +30,11 @@ jobs: - name: Build web bundle with stats run: npx expo export --platform web --output-dir ./dist --stats-output ./dist/stats.json + - name: Lighthouse CI + run: npx lhci autorun --config=./lighthouserc.json + env: + LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }} + - name: Upload bundle stats artifact if: github.ref == 'refs/heads/main' uses: actions/upload-artifact@v4 diff --git a/docs/PERFORMANCE_MONITORING.md b/docs/PERFORMANCE_MONITORING.md index 63e742a4..87561083 100644 --- a/docs/PERFORMANCE_MONITORING.md +++ b/docs/PERFORMANCE_MONITORING.md @@ -11,7 +11,7 @@ render timing, Core Web Vitals, crash reporting, and production analytics. AnalyticsProvider (root) โ”œโ”€โ”€ mobileAnalyticsService โ€“ event ingestion & sampling โ”œโ”€โ”€ crashReportingService โ€“ global JS / promise error handlers -โ””โ”€โ”€ webVitalsService โ€“ LCP, FID, CLS, FCP, TTFB (web target) +โ””โ”€โ”€ webVitalsService โ€“ LCP, INP, CLS, FCP, TTFB (web target) useReactProfiler (hook) โ””โ”€โ”€ Profiler (React built-in) @@ -47,7 +47,7 @@ surfaces that use it contribute to the same monitoring stream. | Metric | Good | Needs Improvement | Poor | Description | |---|---|---|---|---| | LCP | โ‰ค 2500 ms | โ‰ค 4000 ms | > 4000 ms | Largest Contentful Paint | -| FID | โ‰ค 100 ms | โ‰ค 300 ms | > 300 ms | First Input Delay | +| INP | โ‰ค 200 ms | โ‰ค 500 ms | > 500 ms | Interaction to Next Paint | | CLS | โ‰ค 0.1 | โ‰ค 0.25 | > 0.25 | Cumulative Layout Shift | | FCP | โ‰ค 1800 ms | โ‰ค 3000 ms | > 3000 ms | First Contentful Paint | | TTFB | โ‰ค 800 ms | โ‰ค 1800 ms | > 1800 ms | Time to First Byte | diff --git a/lighthouserc.json b/lighthouserc.json index 603f909b..0d0401bf 100644 --- a/lighthouserc.json +++ b/lighthouserc.json @@ -1,17 +1,25 @@ { "ci": { "collect": { - "url": ["http://localhost:3000"], - "numberOfRuns": 3 + "staticDistDir": "./dist", + "numberOfRuns": 3, + "settings": { + "preset": "desktop", + "skipAudits": ["uses-http2"] + } }, "assert": { "assertions": { - "interactive": [ - "error", - { - "maxNumericValue": 2000 - } - ] + "categories:performance": ["error", { "minScore": 0.5 }], + "categories:accessibility": ["warn", { "minScore": 0.8 }], + "categories:best-practices": ["warn", { "minScore": 0.8 }], + "categories:seo": ["warn", { "minScore": 0.8 }], + "first-contentful-paint": ["error", { "maxNumericValue": 3000 }], + "largest-contentful-paint": ["error", { "maxNumericValue": 4000 }], + "cumulative-layout-shift": ["error", { "maxNumericValue": 0.25 }], + "total-blocking-time": ["error", { "maxNumericValue": 3000 }], + "speed-index": ["warn", { "maxNumericValue": 4000 }], + "interactive": ["error", { "maxNumericValue": 5000 }] } }, "upload": { diff --git a/package-lock.json b/package-lock.json index 97a8a09d..25b028ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "teachlink_mobile", - "version": "1.15.0", + "version": "1.16.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "teachlink_mobile", - "version": "1.15.0", + "version": "1.16.4", "dependencies": { "@expo/config-plugins": "^56.0.9", "@expo/vector-icons": "^15.0.3", @@ -39,6 +39,7 @@ "expo-keep-awake": "~15.0.8", "expo-linear-gradient": "^15.0.8", "expo-linking": "~8.0.11", + "expo-local-authentication": "~14.0.1", "expo-location": "^56.0.18", "expo-network": "~8.0.8", "expo-notifications": "~0.32.16", @@ -78,6 +79,7 @@ "@babel/core": "^7.29.0", "@gorhom/bottom-sheet": "^5.2.14", "@jest/globals": "^29.7.0", + "@lhci/cli": "^0.14.0", "@react-native-community/datetimepicker": "^9.1.0", "@react-native-community/slider": "^5.2.0", "@storybook/addon-ondevice-actions": "^10.4.4", @@ -1589,7 +1591,6 @@ "cpu": [ "ppc64" ], - "dev": true, "optional": true, "os": [ "aix" @@ -1605,7 +1606,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "android" @@ -1621,7 +1621,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "android" @@ -1637,7 +1636,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "android" @@ -1653,7 +1651,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -1669,7 +1666,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -1685,7 +1681,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "freebsd" @@ -1701,7 +1696,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "freebsd" @@ -1717,7 +1711,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "linux" @@ -1733,7 +1726,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -1749,7 +1741,6 @@ "cpu": [ "ia32" ], - "dev": true, "optional": true, "os": [ "linux" @@ -1765,7 +1756,6 @@ "cpu": [ "loong64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -1781,7 +1771,6 @@ "cpu": [ "mips64el" ], - "dev": true, "optional": true, "os": [ "linux" @@ -1797,7 +1786,6 @@ "cpu": [ "ppc64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -1813,7 +1801,6 @@ "cpu": [ "riscv64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -1829,7 +1816,6 @@ "cpu": [ "s390x" ], - "dev": true, "optional": true, "os": [ "linux" @@ -1845,7 +1831,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -1861,7 +1846,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "netbsd" @@ -1877,7 +1861,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "netbsd" @@ -1893,7 +1876,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "openbsd" @@ -1909,7 +1891,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "openbsd" @@ -1925,7 +1906,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "openharmony" @@ -1941,7 +1921,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "sunos" @@ -1957,7 +1936,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -1973,7 +1951,6 @@ "cpu": [ "ia32" ], - "dev": true, "optional": true, "os": [ "win32" @@ -1989,7 +1966,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -2427,20 +2403,6 @@ "node": ">=10" } }, - "node_modules/@expo/image-utils/node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "optional": true, - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, "node_modules/@expo/json-file": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.2.0.tgz", @@ -2791,6 +2753,62 @@ "excpretty": "build/cli.js" } }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.6.tgz", + "integrity": "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/intl-localematcher": "0.6.2", + "decimal.js": "^10.4.3", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz", + "integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.4.tgz", + "integrity": "sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.6", + "@formatjs/icu-skeleton-parser": "1.8.16", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "1.8.16", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.16.tgz", + "integrity": "sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.6", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.2.tgz", + "integrity": "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@gorhom/bottom-sheet": { "version": "5.2.14", "resolved": "https://registry.npmjs.org/@gorhom/bottom-sheet/-/bottom-sheet-5.2.14.tgz", @@ -3421,6 +3439,333 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lhci/cli": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@lhci/cli/-/cli-0.14.0.tgz", + "integrity": "sha512-TxOH9pFBnmmN7Jmo2Aimxx5UhE8veqXpHfFJDMWsCVxkwh7mGxcAWchGl84mK139SZbbRmerqZ72c+h2nG9/QQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@lhci/utils": "0.14.0", + "chrome-launcher": "^0.13.4", + "compression": "^1.7.4", + "debug": "^4.3.1", + "express": "^4.17.1", + "inquirer": "^6.3.1", + "isomorphic-fetch": "^3.0.0", + "lighthouse": "12.1.0", + "lighthouse-logger": "1.2.0", + "open": "^7.1.0", + "proxy-agent": "^6.4.0", + "tmp": "^0.1.0", + "uuid": "^8.3.1", + "yargs": "^15.4.1", + "yargs-parser": "^13.1.2" + }, + "bin": { + "lhci": "src/cli.js" + } + }, + "node_modules/@lhci/cli/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@lhci/cli/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@lhci/cli/node_modules/chrome-launcher": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.13.4.tgz", + "integrity": "sha512-nnzXiDbGKjDSK6t2I+35OAPBy5Pw/39bgkb/ZAFwMhwJbdYBp6aH+vW28ZgtjdU890Q7D+3wN/tB8N66q5Gi2A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^1.0.5", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0", + "mkdirp": "^0.5.3", + "rimraf": "^3.0.2" + } + }, + "node_modules/@lhci/cli/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/@lhci/cli/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@lhci/cli/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@lhci/cli/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@lhci/cli/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@lhci/cli/node_modules/lighthouse-logger": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.2.0.tgz", + "integrity": "sha512-wzUvdIeJZhRsG6gpZfmSCfysaxNEr43i+QT+Hie94wvHDKFLi4n7C2GqZ4sTC+PH5b5iktmXJvU87rWvhP3lHw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^2.6.8", + "marky": "^1.2.0" + } + }, + "node_modules/@lhci/cli/node_modules/lighthouse-logger/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/@lhci/cli/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@lhci/cli/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/@lhci/cli/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@lhci/cli/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@lhci/cli/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@lhci/cli/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@lhci/cli/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@lhci/cli/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@lhci/cli/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@lhci/cli/node_modules/yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "node_modules/@lhci/cli/node_modules/yargs/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@lhci/utils": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@lhci/utils/-/utils-0.14.0.tgz", + "integrity": "sha512-LyP1RbvYQ9xNl7uLnl5AO8fDRata9MG/KYfVFKFkYenlsVS6QJsNjLzWNEoMIaE4jOPdQQlSp4tO7dtnyDxzbQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.3.1", + "isomorphic-fetch": "^3.0.0", + "js-yaml": "^3.13.1", + "lighthouse": "12.1.0", + "tree-kill": "^1.2.1" + } + }, + "node_modules/@lhci/utils/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@lhci/utils/node_modules/js-yaml": { + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz", + "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", @@ -4100,6 +4445,49 @@ "win32" ] }, + "node_modules/@paulirish/trace_engine": { + "version": "0.0.23", + "resolved": "https://registry.npmjs.org/@paulirish/trace_engine/-/trace_engine-0.0.23.tgz", + "integrity": "sha512-2ym/q7HhC5K+akXkNV6Gip3oaHpbI6TsGjmcAsl7bcJ528MVbacPQeoauLFEeLXH4ulJvsxQwNDIg/kAEhFZxw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@puppeteer/browsers": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.3.0.tgz", + "integrity": "sha512-ioXoq9gPxkss4MYhD+SFaU9p1IHFUX0ILAWFPyjGaBdjLsYAlZw6j1iLA0N/m12uVHLFDfSYNF7EQccjinIMDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.3.5", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@puppeteer/browsers/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@radix-ui/primitive": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.4.tgz", @@ -4935,6 +5323,134 @@ "node": ">=18" } }, + "node_modules/@sentry/hub": { + "version": "6.19.7", + "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.19.7.tgz", + "integrity": "sha512-y3OtbYFAqKHCWezF0EGGr5lcyI2KbaXW2Ik7Xp8Mu9TxbSTuwTe4rTntwg8ngPjUQU3SUHzgjqVB8qjiGqFXCA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sentry/types": "6.19.7", + "@sentry/utils": "6.19.7", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/hub/node_modules/@sentry/types": { + "version": "6.19.7", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.19.7.tgz", + "integrity": "sha512-jH84pDYE+hHIbVnab3Hr+ZXr1v8QABfhx39KknxqKWr2l0oEItzepV0URvbEhB446lk/S/59230dlUUIBGsXbg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/hub/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@sentry/minimal": { + "version": "6.19.7", + "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.19.7.tgz", + "integrity": "sha512-wcYmSJOdvk6VAPx8IcmZgN08XTXRwRtB1aOLZm+MVHjIZIhHoBGZJYTVQS/BWjldsamj2cX3YGbGXNunaCfYJQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sentry/hub": "6.19.7", + "@sentry/types": "6.19.7", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/minimal/node_modules/@sentry/types": { + "version": "6.19.7", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.19.7.tgz", + "integrity": "sha512-jH84pDYE+hHIbVnab3Hr+ZXr1v8QABfhx39KknxqKWr2l0oEItzepV0URvbEhB446lk/S/59230dlUUIBGsXbg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/minimal/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@sentry/node": { + "version": "6.19.7", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-6.19.7.tgz", + "integrity": "sha512-gtmRC4dAXKODMpHXKfrkfvyBL3cI8y64vEi3fDD046uqYcrWdgoQsffuBbxMAizc6Ez1ia+f0Flue6p15Qaltg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sentry/core": "6.19.7", + "@sentry/hub": "6.19.7", + "@sentry/types": "6.19.7", + "@sentry/utils": "6.19.7", + "cookie": "^0.4.1", + "https-proxy-agent": "^5.0.0", + "lru_map": "^0.3.3", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/node/node_modules/@sentry/core": { + "version": "6.19.7", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.19.7.tgz", + "integrity": "sha512-tOfZ/umqB2AcHPGbIrsFLcvApdTm9ggpi/kQZFkej7kMphjT+SGBiQfYtjyg9jcRW+ilAR4JXC9BGKsdEQ+8Vw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sentry/hub": "6.19.7", + "@sentry/minimal": "6.19.7", + "@sentry/types": "6.19.7", + "@sentry/utils": "6.19.7", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/node/node_modules/@sentry/types": { + "version": "6.19.7", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.19.7.tgz", + "integrity": "sha512-jH84pDYE+hHIbVnab3Hr+ZXr1v8QABfhx39KknxqKWr2l0oEItzepV0URvbEhB446lk/S/59230dlUUIBGsXbg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/node/node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sentry/node/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, "node_modules/@sentry/react": { "version": "10.12.0", "resolved": "https://registry.npmjs.org/@sentry/react/-/react-10.12.0.tgz", @@ -4991,6 +5507,37 @@ "node": ">=18" } }, + "node_modules/@sentry/utils": { + "version": "6.19.7", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.19.7.tgz", + "integrity": "sha512-z95ECmE3i9pbWoXQrD/7PgkBAzJYR+iXtPuTkpBjDKs86O3mT+PXOT3BAn79w2wkn7/i3vOGD2xVr1uiMl26dA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sentry/types": "6.19.7", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/utils/node_modules/@sentry/types": { + "version": "6.19.7", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.19.7.tgz", + "integrity": "sha512-jH84pDYE+hHIbVnab3Hr+ZXr1v8QABfhx39KknxqKWr2l0oEItzepV0URvbEhB446lk/S/59230dlUUIBGsXbg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/utils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, "node_modules/@sinclair/typebox": { "version": "0.27.10", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", @@ -5520,6 +6067,13 @@ "node": ">= 10" } }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true, + "license": "MIT" + }, "node_modules/@tybys/wasm-util": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", @@ -5751,6 +6305,17 @@ "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==" }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.0.tgz", @@ -6607,6 +7172,16 @@ "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz", "integrity": "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==" }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -6714,6 +7289,13 @@ "node": ">=8" } }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true, + "license": "MIT" + }, "node_modules/array-includes": { "version": "3.1.9", "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", @@ -6995,6 +7577,21 @@ "node": ">= 0.4" } }, + "node_modules/b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -7216,6 +7813,91 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/bare-events": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.2.tgz", + "integrity": "sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.1.tgz", + "integrity": "sha512-N1nnXdHZAOSstz0XiHikGS4HGMH4CnSwhqWdGQQMqqdvp4Jybm9sE3R1WVnpWVd4SFkc8ryPDBLViNLwiEqECg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.28.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.1.tgz", + "integrity": "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/bare-stream": { + "version": "2.13.4", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.4.tgz", + "integrity": "sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.8.1", + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.2.tgz", + "integrity": "sha512-L13PCJzKG8RGvx8V1/DdMi12ERhC3tprr7/8a94BxpmnRsFqxh5XZNdhtMxu5HPkRshYOOWRGY8lDP7ZhpG9Cg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -7246,6 +7928,16 @@ "node": ">=6.0.0" } }, + "node_modules/basic-ftp": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", + "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/better-opn": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/better-opn/-/better-opn-3.0.2.tgz", @@ -7292,6 +7984,74 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/body-parser": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/body-parser/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -7399,6 +8159,16 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -7582,6 +8352,13 @@ "node": ">=10" } }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true, + "license": "MIT" + }, "node_modules/check-error": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", @@ -7650,6 +8427,31 @@ "node": ">=12.13.0" } }, + "node_modules/chromium-bidi": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.3.tgz", + "integrity": "sha512-qXlsCmpCZJAnoTYI83Iu6EdYQpMYdVkCfq08KDh2pmlVqK5t5IA9mGs4/LwCwp4fqisSOMXZxP3HIh8w8aRn0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mitt": "3.0.1", + "urlpattern-polyfill": "10.0.0", + "zod": "3.23.8" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/chromium-bidi/node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/chromium-edge-launcher": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/chromium-edge-launcher/-/chromium-edge-launcher-0.2.0.tgz", @@ -7725,6 +8527,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-width": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", + "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", + "dev": true, + "license": "ISC" + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -7961,6 +8770,53 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/configstore/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/configstore/node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, "node_modules/connect": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", @@ -7988,11 +8844,51 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "dev": true, + "license": "MIT" + }, "node_modules/core-js-compat": { "version": "3.49.0", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz", @@ -8073,6 +8969,23 @@ "node": ">= 8" } }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/csp_evaluator": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/csp_evaluator/-/csp_evaluator-1.1.1.tgz", + "integrity": "sha512-N3ASg0C4kNPUaNxt1XAvzHIVuzdtr8KLgfk1O8WDyimp1GisPAHESupArO2ieHk9QWbrJ/WkQODyh21Ps/xhxw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/css-in-js-utils": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz", @@ -8172,6 +9085,16 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/data-urls": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", @@ -8253,6 +9176,16 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/decimal.js": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", @@ -8391,6 +9324,34 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/degenerator/node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -8500,13 +9461,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/depcheck/node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "dev": true, - "license": "MIT" - }, "node_modules/depcheck/node_modules/semver": { "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", @@ -8644,6 +9598,13 @@ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" }, + "node_modules/devtools-protocol": { + "version": "0.0.1312386", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1312386.tgz", + "integrity": "sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -8757,6 +9718,19 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/dotenv": { "version": "16.4.7", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", @@ -8831,6 +9805,16 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/engine.io-client": { "version": "6.6.6", "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.6.tgz", @@ -8851,6 +9835,20 @@ "node": ">=10.0.0" } }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/entities": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", @@ -9728,6 +10726,16 @@ "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "dev": true }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -10103,6 +11111,18 @@ "react-native": "*" } }, + "node_modules/expo-local-authentication": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/expo-local-authentication/-/expo-local-authentication-14.0.1.tgz", + "integrity": "sha512-kAwUD1wEqj1fhwQgIHlP4H/JV9AcX+NO3BJwhPM2HuCFS0kgx2wvcHisnKBSTRyl8u5Jt4odzMyQkDJystwUTg==", + "license": "MIT", + "dependencies": { + "invariant": "^2.2.4" + }, + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-location": { "version": "56.0.18", "resolved": "https://registry.npmjs.org/expo-location/-/expo-location-56.0.18.tgz", @@ -11160,11 +12180,223 @@ "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==" }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/express/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/external-editor/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -11285,6 +12517,39 @@ "node": "*" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -11454,6 +12719,16 @@ "node": ">= 6" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/freeport-async": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/freeport-async/-/freeport-async-2.0.0.tgz", @@ -11656,6 +12931,21 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/getenv": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/getenv/-/getenv-2.0.0.tgz", @@ -11972,6 +13262,16 @@ "node": ">= 0.8" } }, + "node_modules/http-link-header": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/http-link-header/-/http-link-header-1.1.4.tgz", + "integrity": "sha512-xT3GPW6/ZbGuw4UvwHqErSCEjNUlwbQJuZn9/q5U4WEKfp2kENVCAlousG1zLxHeaQ/ffOHUNpWamvkbBW0eNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -12080,6 +13380,13 @@ "node": ">=16.x" } }, + "node_modules/image-ssim": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/image-ssim/-/image-ssim-0.2.0.tgz", + "integrity": "sha512-W7+sO6/yhxy83L0G7xR8YAc5Z5QFtYEXXRV6EaE8tuYBZJnA3gVgp3q7X7muhLZVodeb9UfvjSbwt9VJwjIYAg==", + "dev": true, + "license": "MIT" + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -12169,6 +13476,239 @@ "css-in-js-utils": "^3.1.0" } }, + "node_modules/inquirer": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", + "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^3.2.0", + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^2.0.0", + "lodash": "^4.17.12", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^6.4.0", + "string-width": "^2.1.0", + "strip-ansi": "^5.1.0", + "through": "^2.3.6" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/inquirer/node_modules/ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/inquirer/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/inquirer/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/inquirer/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/inquirer/node_modules/cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/inquirer/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/inquirer/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/inquirer/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/inquirer/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/inquirer/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/inquirer/node_modules/mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/inquirer/node_modules/onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/inquirer/node_modules/restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/inquirer/node_modules/string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/inquirer/node_modules/string-width/node_modules/ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/inquirer/node_modules/string-width/node_modules/strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/inquirer/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/inquirer/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -12183,6 +13723,19 @@ "node": ">= 0.4" } }, + "node_modules/intl-messageformat": { + "version": "10.7.18", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.18.tgz", + "integrity": "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.6", + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/icu-messageformat-parser": "2.11.4", + "tslib": "^2.8.0" + } + }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -12191,6 +13744,26 @@ "loose-envify": "^1.0.0" } }, + "node_modules/ip-address": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz", + "integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-arguments": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", @@ -12593,6 +14166,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", @@ -12710,6 +14293,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true, + "license": "MIT" + }, "node_modules/is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -12784,6 +14374,17 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, + "node_modules/isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -13269,13 +14870,6 @@ } } }, - "node_modules/jest-expo/node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "dev": true, - "license": "MIT" - }, "node_modules/jest-get-type": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", @@ -13902,6 +15496,23 @@ "jiti": "bin/jiti.js" } }, + "node_modules/jpeg-js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", + "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/js-library-detector": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/js-library-detector/-/js-library-detector-6.7.0.tgz", + "integrity": "sha512-c80Qupofp43y4cJ7+8TTDN/AsDwLi5oOm/plBrWI+iQt485vKXCco+yVmOwEgdo9VOdsYTuV0UlTeetVPTriXA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -14109,6 +15720,51 @@ "node": ">= 0.8.0" } }, + "node_modules/lighthouse": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/lighthouse/-/lighthouse-12.1.0.tgz", + "integrity": "sha512-PQLaNcv3tQcybnYux6T8uoS6+RNrNYvVJBbGo0kkbD4XTjesGslOXWeMkUQDK7c28nLfVZi7gYWDUsicTLglKQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@paulirish/trace_engine": "^0.0.23", + "@sentry/node": "^6.17.4", + "axe-core": "^4.9.1", + "chrome-launcher": "^1.1.2", + "configstore": "^5.0.1", + "csp_evaluator": "1.1.1", + "devtools-protocol": "0.0.1312386", + "enquirer": "^2.3.6", + "http-link-header": "^1.1.1", + "intl-messageformat": "^10.5.3", + "jpeg-js": "^0.4.4", + "js-library-detector": "^6.7.0", + "lighthouse-logger": "^2.0.1", + "lighthouse-stack-packs": "1.12.1", + "lodash": "^4.17.21", + "lookup-closest-locale": "6.2.0", + "metaviewport-parser": "0.3.0", + "open": "^8.4.0", + "parse-cache-control": "1.0.1", + "puppeteer-core": "^22.11.1", + "robots-parser": "^3.0.1", + "semver": "^5.3.0", + "speedline-core": "^1.4.3", + "third-party-web": "^0.24.3", + "tldts-icann": "^6.1.16", + "ws": "^7.0.0", + "yargs": "^17.3.1", + "yargs-parser": "^21.0.0" + }, + "bin": { + "chrome-debug": "core/scripts/manual-chrome-launcher.js", + "lighthouse": "cli/index.js", + "smokehouse": "cli/test/smokehouse/frontends/smokehouse-bin.js" + }, + "engines": { + "node": ">=18.16" + } + }, "node_modules/lighthouse-logger": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz", @@ -14131,6 +15787,71 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/lighthouse-stack-packs": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/lighthouse-stack-packs/-/lighthouse-stack-packs-1.12.1.tgz", + "integrity": "sha512-i4jTmg7tvZQFwNFiwB+nCK6a7ICR68Xcwo+VIVd6Spi71vBNFUlds5HiDrSbClZdkQDON2Bhqv+KKJIo5zkPeA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/lighthouse/node_modules/chrome-launcher": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-1.2.1.tgz", + "integrity": "sha512-qmFR5PLMzHyuNJHwOloHPAHhbaNglkfeV/xDtt5b7xiFFyU1I+AZZX0PYseMuhenJSSirgxELYIbswcoc+5H4A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^2.0.1" + }, + "bin": { + "print-chrome-path": "bin/print-chrome-path.cjs" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/lighthouse/node_modules/lighthouse-logger": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-2.0.2.tgz", + "integrity": "sha512-vWl2+u5jgOQuZR55Z1WM0XDdrJT6mzMP8zHUct7xTlWhuQs+eV0g+QL0RQdFjT54zVmbhLCP8vIVpy1wGn/gCg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.1", + "marky": "^1.2.2" + } + }, + "node_modules/lighthouse/node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lighthouse/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/lightningcss": { "version": "1.27.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.27.0.tgz", @@ -14469,6 +16190,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -14649,6 +16377,13 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/lookup-closest-locale": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/lookup-closest-locale/-/lookup-closest-locale-6.2.0.tgz", + "integrity": "sha512-/c2kL+Vnp1jnV6K6RpDTHK3dgg0Tu2VVp+elEiJpjfS1UyY7AjOYHohRug6wT0OpoX2qFgNORndE9RqesfVxWQ==", + "dev": true, + "license": "MIT" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -14666,6 +16401,13 @@ "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", "dev": true }, + "node_modules/lru_map": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz", + "integrity": "sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -14762,6 +16504,16 @@ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/memoize-one": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", @@ -14776,6 +16528,16 @@ "map-or-similar": "^1.5.0" } }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge-options": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz", @@ -14800,6 +16562,23 @@ "node": ">= 8" } }, + "node_modules/metaviewport-parser": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/metaviewport-parser/-/metaviewport-parser-0.3.0.tgz", + "integrity": "sha512-EoYJ8xfjQ6kpe9VbVHvZTZHiOl4HL1Z18CrZ+qahvLXT7ZO4YTC2JMyt5FaUp9JJp6J4Ybb/z7IsCXZt86/QkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/metro": { "version": "0.83.7", "resolved": "https://registry.npmjs.org/metro/-/metro-0.83.7.tgz", @@ -15697,6 +17476,13 @@ "node": ">= 18" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -15744,6 +17530,13 @@ "node": "*" } }, + "node_modules/mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==", + "dev": true, + "license": "ISC" + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -15821,6 +17614,16 @@ "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.0.1.tgz", "integrity": "sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==" }, + "node_modules/netmask": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", + "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/node-exports-info": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz", @@ -16447,6 +18250,78 @@ "node": ">=6" } }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -16459,6 +18334,12 @@ "node": ">=6" } }, + "node_modules/parse-cache-control": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", + "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==", + "dev": true + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -16581,6 +18462,13 @@ "node": "20 || >=22" } }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "dev": true, + "license": "MIT" + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -16599,6 +18487,13 @@ "node": ">= 14.16" } }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -17036,6 +18931,88 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "dev": true }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -17054,6 +19031,17 @@ "url": "https://github.com/sponsors/lupomontero" } }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -17062,6 +19050,23 @@ "node": ">=6" } }, + "node_modules/puppeteer-core": { + "version": "22.15.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.15.0.tgz", + "integrity": "sha512-cHArnywCiAAVXa3t4GGL2vttNxh7GqXtIYGym99egkNJ3oG//wL9LkvO4WE8W1TJe95t1F1ocu9X4xWaGsOKOA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.3.0", + "chromium-bidi": "0.6.3", + "debug": "^4.3.6", + "devtools-protocol": "0.0.1312386", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/pure-rand": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", @@ -17086,6 +19091,23 @@ "qrcode-terminal": "bin/qrcode-terminal.js" } }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/query-string": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", @@ -17144,6 +19166,35 @@ "node": ">= 0.6" } }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -17985,6 +20036,13 @@ "node": ">=0.10.0" } }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true, + "license": "ISC" + }, "node_modules/require-package-name": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/require-package-name/-/require-package-name-2.0.1.tgz", @@ -18197,6 +20255,16 @@ "node": "*" } }, + "node_modules/robots-parser": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/robots-parser/-/robots-parser-3.0.1.tgz", + "integrity": "sha512-s+pyvQeIKIZ0dx5iJiQk1tPLJAWln39+MI5jtM8wnyws+G5azk+dMnMX0qfbqNetKKNgcWWOdi0sfm+FbQbgdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/run-applescript": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", @@ -18209,6 +20277,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -18231,6 +20309,26 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/rxjs/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, "node_modules/safe-array-concat": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", @@ -18444,6 +20542,13 @@ "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz", "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==" }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true, + "license": "ISC" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -18691,6 +20796,17 @@ "node": ">=8.0.0" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, "node_modules/socket.io-client": { "version": "4.8.3", "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", @@ -18717,6 +20833,46 @@ "node": ">=10.0.0" } }, + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/socks-proxy-agent/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -18743,6 +20899,21 @@ "source-map": "^0.6.0" } }, + "node_modules/speedline-core": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/speedline-core/-/speedline-core-1.4.3.tgz", + "integrity": "sha512-DI7/OuAUD+GMpR6dmu8lliO2Wg5zfeh+/xsdyJZCzd8o5JgFUjCeLsBDuZjIQJdwXS3J0L/uZYrELKYqx+PXog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "image-ssim": "^0.2.0", + "jpeg-js": "^0.4.1" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/split-on-first": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", @@ -18972,6 +21143,18 @@ "node": ">= 0.10.0" } }, + "node_modules/streamx": { + "version": "2.28.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.1.tgz", + "integrity": "sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, "node_modules/strict-uri-encode": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", @@ -19377,6 +21560,34 @@ "node": ">=18" } }, + "node_modules/tar-fs": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", + "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.1.tgz", + "integrity": "sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, "node_modules/tar/node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", @@ -19385,6 +21596,16 @@ "node": ">=18" } }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, "node_modules/terminal-link": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", @@ -19475,6 +21696,16 @@ "node": "*" } }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -19494,11 +21725,25 @@ "node": ">=0.8" } }, + "node_modules/third-party-web": { + "version": "0.24.5", + "resolved": "https://registry.npmjs.org/third-party-web/-/third-party-web-0.24.5.tgz", + "integrity": "sha512-1rUOdMYpNTRajgk1F7CmHD26oA6rTKekBjHay854J6OkPXeNyPcR54rhWDaamlWyi9t2wAVPQESdedBhucmOLA==", + "dev": true, + "license": "MIT" + }, "node_modules/throat": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==" }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -19580,6 +21825,23 @@ "node": ">=14.0.0" } }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tldts-icann": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-icann/-/tldts-icann-6.1.86.tgz", + "integrity": "sha512-NFxmRT2lAEMcCOBgeZ0NuM0zsK/xgmNajnY6n4S1mwAKocft2s2ise1O3nQxrH3c+uY6hgHUV9GGNVp7tUE4Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + } + }, "node_modules/tmcp": { "version": "1.19.4", "resolved": "https://registry.npmjs.org/tmcp/-/tmcp-1.19.4.tgz", @@ -19593,6 +21855,16 @@ "valibot": "^1.1.0" } }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -19644,6 +21916,16 @@ "node": ">=12" } }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -19757,6 +22039,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -19831,6 +22127,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, "node_modules/typescript": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", @@ -19887,6 +22193,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, "node_modules/undici": { "version": "6.27.0", "resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz", @@ -19936,6 +22253,19 @@ "node": ">=4" } }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/universalify": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", @@ -20044,6 +22374,13 @@ "requires-port": "^1.0.0" } }, + "node_modules/urlpattern-polyfill": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", + "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==", + "dev": true, + "license": "MIT" + }, "node_modules/use-callback-ref": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", @@ -20569,6 +22906,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "dev": true, + "license": "ISC" + }, "node_modules/which-typed-array": { "version": "1.1.22", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", @@ -20761,6 +23105,16 @@ "node": ">=10.0.0" } }, + "node_modules/xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/xml-name-validator": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", @@ -20891,6 +23245,17 @@ "node": ">=8" } }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -21909,182 +24274,156 @@ "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", - "dev": true, "optional": true }, "@esbuild/android-arm": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", - "dev": true, "optional": true }, "@esbuild/android-arm64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", - "dev": true, "optional": true }, "@esbuild/android-x64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", - "dev": true, "optional": true }, "@esbuild/darwin-arm64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", - "dev": true, "optional": true }, "@esbuild/darwin-x64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", - "dev": true, "optional": true }, "@esbuild/freebsd-arm64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", - "dev": true, "optional": true }, "@esbuild/freebsd-x64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", - "dev": true, "optional": true }, "@esbuild/linux-arm": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", - "dev": true, "optional": true }, "@esbuild/linux-arm64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", - "dev": true, "optional": true }, "@esbuild/linux-ia32": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", - "dev": true, "optional": true }, "@esbuild/linux-loong64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", - "dev": true, "optional": true }, "@esbuild/linux-mips64el": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", - "dev": true, "optional": true }, "@esbuild/linux-ppc64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", - "dev": true, "optional": true }, "@esbuild/linux-riscv64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", - "dev": true, "optional": true }, "@esbuild/linux-s390x": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", - "dev": true, "optional": true }, "@esbuild/linux-x64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", - "dev": true, "optional": true }, "@esbuild/netbsd-arm64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", - "dev": true, "optional": true }, "@esbuild/netbsd-x64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", - "dev": true, "optional": true }, "@esbuild/openbsd-arm64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", - "dev": true, "optional": true }, "@esbuild/openbsd-x64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", - "dev": true, "optional": true }, "@esbuild/openharmony-arm64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", - "dev": true, "optional": true }, "@esbuild/sunos-x64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", - "dev": true, "optional": true }, "@esbuild/win32-arm64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", - "dev": true, "optional": true }, "@esbuild/win32-ia32": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", - "dev": true, "optional": true }, "@esbuild/win32-x64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", - "dev": true, "optional": true }, "@eslint-community/eslint-utils": { @@ -22420,13 +24759,6 @@ "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==" - }, - "typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "optional": true, - "peer": true } } }, @@ -22721,6 +25053,57 @@ "js-yaml": "^4.1.0" } }, + "@formatjs/ecma402-abstract": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.6.tgz", + "integrity": "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==", + "dev": true, + "requires": { + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/intl-localematcher": "0.6.2", + "decimal.js": "^10.4.3", + "tslib": "^2.8.0" + } + }, + "@formatjs/fast-memoize": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz", + "integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==", + "dev": true, + "requires": { + "tslib": "^2.8.0" + } + }, + "@formatjs/icu-messageformat-parser": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.4.tgz", + "integrity": "sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==", + "dev": true, + "requires": { + "@formatjs/ecma402-abstract": "2.3.6", + "@formatjs/icu-skeleton-parser": "1.8.16", + "tslib": "^2.8.0" + } + }, + "@formatjs/icu-skeleton-parser": { + "version": "1.8.16", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.16.tgz", + "integrity": "sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==", + "dev": true, + "requires": { + "@formatjs/ecma402-abstract": "2.3.6", + "tslib": "^2.8.0" + } + }, + "@formatjs/intl-localematcher": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.2.tgz", + "integrity": "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==", + "dev": true, + "requires": { + "tslib": "^2.8.0" + } + }, "@gorhom/bottom-sheet": { "version": "5.2.14", "resolved": "https://registry.npmjs.org/@gorhom/bottom-sheet/-/bottom-sheet-5.2.14.tgz", @@ -23195,6 +25578,265 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "@lhci/cli": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@lhci/cli/-/cli-0.14.0.tgz", + "integrity": "sha512-TxOH9pFBnmmN7Jmo2Aimxx5UhE8veqXpHfFJDMWsCVxkwh7mGxcAWchGl84mK139SZbbRmerqZ72c+h2nG9/QQ==", + "dev": true, + "requires": { + "@lhci/utils": "0.14.0", + "chrome-launcher": "^0.13.4", + "compression": "^1.7.4", + "debug": "^4.3.1", + "express": "^4.17.1", + "inquirer": "^6.3.1", + "isomorphic-fetch": "^3.0.0", + "lighthouse": "12.1.0", + "lighthouse-logger": "1.2.0", + "open": "^7.1.0", + "proxy-agent": "^6.4.0", + "tmp": "^0.2.6", + "uuid": "^11.0.3", + "yargs": "^15.4.1", + "yargs-parser": "^13.1.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "chrome-launcher": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.13.4.tgz", + "integrity": "sha512-nnzXiDbGKjDSK6t2I+35OAPBy5Pw/39bgkb/ZAFwMhwJbdYBp6aH+vW28ZgtjdU890Q7D+3wN/tB8N66q5Gi2A==", + "dev": true, + "requires": { + "@types/node": "*", + "escape-string-regexp": "^1.0.5", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0", + "mkdirp": "^0.5.3", + "rimraf": "^3.0.2" + } + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "lighthouse-logger": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.2.0.tgz", + "integrity": "sha512-wzUvdIeJZhRsG6gpZfmSCfysaxNEr43i+QT+Hie94wvHDKFLi4n7C2GqZ4sTC+PH5b5iktmXJvU87rWvhP3lHw==", + "dev": true, + "requires": { + "debug": "^2.6.8", + "marky": "^1.2.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "requires": { + "minimist": "^1.2.6" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "dependencies": { + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "@lhci/utils": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@lhci/utils/-/utils-0.14.0.tgz", + "integrity": "sha512-LyP1RbvYQ9xNl7uLnl5AO8fDRata9MG/KYfVFKFkYenlsVS6QJsNjLzWNEoMIaE4jOPdQQlSp4tO7dtnyDxzbQ==", + "dev": true, + "requires": { + "debug": "^4.3.1", + "isomorphic-fetch": "^3.0.0", + "js-yaml": "^3.13.1", + "lighthouse": "12.1.0", + "tree-kill": "^1.2.1" + }, + "dependencies": { + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "js-yaml": { + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz", + "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + } + } + }, "@napi-rs/wasm-runtime": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", @@ -23562,6 +26204,36 @@ "dev": true, "optional": true }, + "@paulirish/trace_engine": { + "version": "0.0.23", + "resolved": "https://registry.npmjs.org/@paulirish/trace_engine/-/trace_engine-0.0.23.tgz", + "integrity": "sha512-2ym/q7HhC5K+akXkNV6Gip3oaHpbI6TsGjmcAsl7bcJ528MVbacPQeoauLFEeLXH4ulJvsxQwNDIg/kAEhFZxw==", + "dev": true + }, + "@puppeteer/browsers": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.3.0.tgz", + "integrity": "sha512-ioXoq9gPxkss4MYhD+SFaU9p1IHFUX0ILAWFPyjGaBdjLsYAlZw6j1iLA0N/m12uVHLFDfSYNF7EQccjinIMDA==", + "dev": true, + "requires": { + "debug": "^4.3.5", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + }, + "dependencies": { + "semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true + } + } + }, "@radix-ui/primitive": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.4.tgz", @@ -24057,6 +26729,105 @@ "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.12.0.tgz", "integrity": "sha512-Jrf0Yo7DvmI/ZQcvBnA0xKNAFkJlVC/fMlvcin+5IrFNRcqOToZ2vtF+XqTgjRZymXQNE8s1QTD7IomPHk0TAw==" }, + "@sentry/hub": { + "version": "6.19.7", + "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.19.7.tgz", + "integrity": "sha512-y3OtbYFAqKHCWezF0EGGr5lcyI2KbaXW2Ik7Xp8Mu9TxbSTuwTe4rTntwg8ngPjUQU3SUHzgjqVB8qjiGqFXCA==", + "dev": true, + "requires": { + "@sentry/types": "6.19.7", + "@sentry/utils": "6.19.7", + "tslib": "^1.9.3" + }, + "dependencies": { + "@sentry/types": { + "version": "6.19.7", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.19.7.tgz", + "integrity": "sha512-jH84pDYE+hHIbVnab3Hr+ZXr1v8QABfhx39KknxqKWr2l0oEItzepV0URvbEhB446lk/S/59230dlUUIBGsXbg==", + "dev": true + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, + "@sentry/minimal": { + "version": "6.19.7", + "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.19.7.tgz", + "integrity": "sha512-wcYmSJOdvk6VAPx8IcmZgN08XTXRwRtB1aOLZm+MVHjIZIhHoBGZJYTVQS/BWjldsamj2cX3YGbGXNunaCfYJQ==", + "dev": true, + "requires": { + "@sentry/hub": "6.19.7", + "@sentry/types": "6.19.7", + "tslib": "^1.9.3" + }, + "dependencies": { + "@sentry/types": { + "version": "6.19.7", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.19.7.tgz", + "integrity": "sha512-jH84pDYE+hHIbVnab3Hr+ZXr1v8QABfhx39KknxqKWr2l0oEItzepV0URvbEhB446lk/S/59230dlUUIBGsXbg==", + "dev": true + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, + "@sentry/node": { + "version": "6.19.7", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-6.19.7.tgz", + "integrity": "sha512-gtmRC4dAXKODMpHXKfrkfvyBL3cI8y64vEi3fDD046uqYcrWdgoQsffuBbxMAizc6Ez1ia+f0Flue6p15Qaltg==", + "dev": true, + "requires": { + "@sentry/core": "6.19.7", + "@sentry/hub": "6.19.7", + "@sentry/types": "6.19.7", + "@sentry/utils": "6.19.7", + "cookie": "^0.4.1", + "https-proxy-agent": "^5.0.0", + "lru_map": "^0.3.3", + "tslib": "^1.9.3" + }, + "dependencies": { + "@sentry/core": { + "version": "6.19.7", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.19.7.tgz", + "integrity": "sha512-tOfZ/umqB2AcHPGbIrsFLcvApdTm9ggpi/kQZFkej7kMphjT+SGBiQfYtjyg9jcRW+ilAR4JXC9BGKsdEQ+8Vw==", + "dev": true, + "requires": { + "@sentry/hub": "6.19.7", + "@sentry/minimal": "6.19.7", + "@sentry/types": "6.19.7", + "@sentry/utils": "6.19.7", + "tslib": "^1.9.3" + } + }, + "@sentry/types": { + "version": "6.19.7", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.19.7.tgz", + "integrity": "sha512-jH84pDYE+hHIbVnab3Hr+ZXr1v8QABfhx39KknxqKWr2l0oEItzepV0URvbEhB446lk/S/59230dlUUIBGsXbg==", + "dev": true + }, + "cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "dev": true + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, "@sentry/react": { "version": "10.12.0", "resolved": "https://registry.npmjs.org/@sentry/react/-/react-10.12.0.tgz", @@ -24088,6 +26859,30 @@ "@sentry/core": "10.12.0" } }, + "@sentry/utils": { + "version": "6.19.7", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.19.7.tgz", + "integrity": "sha512-z95ECmE3i9pbWoXQrD/7PgkBAzJYR+iXtPuTkpBjDKs86O3mT+PXOT3BAn79w2wkn7/i3vOGD2xVr1uiMl26dA==", + "dev": true, + "requires": { + "@sentry/types": "6.19.7", + "tslib": "^1.9.3" + }, + "dependencies": { + "@sentry/types": { + "version": "6.19.7", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.19.7.tgz", + "integrity": "sha512-jH84pDYE+hHIbVnab3Hr+ZXr1v8QABfhx39KknxqKWr2l0oEItzepV0URvbEhB446lk/S/59230dlUUIBGsXbg==", + "dev": true + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, "@sinclair/typebox": { "version": "0.27.10", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", @@ -24437,6 +27232,12 @@ "integrity": "sha512-VyMVKRrpHTT8PnotUeV8L/mDaMwD5DaAKCFLP73zAqAtvF0FCqky+Ki7BYbFCYQmqFyTe9316Ed5zS70QUR9eg==", "dev": true }, + "@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true + }, "@tybys/wasm-util": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", @@ -24663,6 +27464,16 @@ "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==" }, + "@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "optional": true, + "requires": { + "@types/node": "*" + } + }, "@typescript-eslint/eslint-plugin": { "version": "8.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.0.tgz", @@ -25226,6 +28037,12 @@ "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz", "integrity": "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==" }, + "ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true + }, "ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -25301,6 +28118,12 @@ "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==", "dev": true }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true + }, "array-includes": { "version": "3.1.9", "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", @@ -25511,6 +28334,13 @@ "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", "dev": true }, + "b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "dev": true, + "requires": {} + }, "babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -25688,6 +28518,52 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "bare-events": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.2.tgz", + "integrity": "sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==", + "dev": true, + "requires": {} + }, + "bare-fs": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.1.tgz", + "integrity": "sha512-N1nnXdHZAOSstz0XiHikGS4HGMH4CnSwhqWdGQQMqqdvp4Jybm9sE3R1WVnpWVd4SFkc8ryPDBLViNLwiEqECg==", + "dev": true, + "requires": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + } + }, + "bare-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.1.tgz", + "integrity": "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==", + "dev": true + }, + "bare-stream": { + "version": "2.13.4", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.4.tgz", + "integrity": "sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==", + "dev": true, + "requires": { + "b4a": "^1.8.1", + "streamx": "^2.25.0", + "teex": "^1.0.1" + } + }, + "bare-url": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.2.tgz", + "integrity": "sha512-L13PCJzKG8RGvx8V1/DdMi12ERhC3tprr7/8a94BxpmnRsFqxh5XZNdhtMxu5HPkRshYOOWRGY8lDP7ZhpG9Cg==", + "dev": true, + "requires": { + "bare-path": "^3.0.0" + } + }, "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -25698,6 +28574,12 @@ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz", "integrity": "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==" }, + "basic-ftp": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", + "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", + "dev": true + }, "better-opn": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/better-opn/-/better-opn-3.0.2.tgz", @@ -25728,6 +28610,61 @@ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==" }, + "body-parser": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", + "dev": true, + "requires": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + } + } + }, "boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -25795,6 +28732,12 @@ "ieee754": "^1.1.13" } }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true + }, "buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -25908,6 +28851,12 @@ "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", "devOptional": true }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, "check-error": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", @@ -25955,6 +28904,25 @@ "lighthouse-logger": "^1.0.0" } }, + "chromium-bidi": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.3.tgz", + "integrity": "sha512-qXlsCmpCZJAnoTYI83Iu6EdYQpMYdVkCfq08KDh2pmlVqK5t5IA9mGs4/LwCwp4fqisSOMXZxP3HIh8w8aRn0A==", + "dev": true, + "requires": { + "mitt": "3.0.1", + "urlpattern-polyfill": "10.0.0", + "zod": "3.23.8" + }, + "dependencies": { + "zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "dev": true + } + } + }, "chromium-edge-launcher": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/chromium-edge-launcher/-/chromium-edge-launcher-0.2.0.tgz", @@ -26003,6 +28971,12 @@ "string-width": "^8.2.0" } }, + "cli-width": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", + "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", + "dev": true + }, "client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -26187,6 +29161,43 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "dev": true, + "requires": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + }, + "dependencies": { + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + } + } + }, "connect": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", @@ -26213,11 +29224,38 @@ } } }, + "content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "requires": { + "safe-buffer": "5.2.1" + } + }, + "content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true + }, "convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" }, + "cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true + }, + "cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "dev": true + }, "core-js-compat": { "version": "3.49.0", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz", @@ -26280,6 +29318,18 @@ "which": "^2.0.1" } }, + "crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "dev": true + }, + "csp_evaluator": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/csp_evaluator/-/csp_evaluator-1.1.1.tgz", + "integrity": "sha512-N3ASg0C4kNPUaNxt1XAvzHIVuzdtr8KLgfk1O8WDyimp1GisPAHESupArO2ieHk9QWbrJ/WkQODyh21Ps/xhxw==", + "dev": true + }, "css-in-js-utils": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz", @@ -26360,6 +29410,12 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, + "data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "dev": true + }, "data-urls": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", @@ -26412,6 +29468,12 @@ "ms": "^2.1.3" } }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true + }, "decimal.js": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", @@ -26501,6 +29563,28 @@ "object-keys": "^1.1.1" } }, + "degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dev": true, + "requires": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "dependencies": { + "ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "requires": { + "tslib": "^2.0.1" + } + } + } + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -26588,12 +29672,6 @@ "esprima": "^4.0.0" } }, - "lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "dev": true - }, "semver": { "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", @@ -26689,6 +29767,12 @@ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" }, + "devtools-protocol": { + "version": "0.0.1312386", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1312386.tgz", + "integrity": "sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA==", + "dev": true + }, "didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -26769,6 +29853,15 @@ "domhandler": "^5.0.3" } }, + "dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "requires": { + "is-obj": "^2.0.0" + } + }, "dotenv": { "version": "16.4.7", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", @@ -26819,6 +29912,15 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" }, + "end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, "engine.io-client": { "version": "6.6.6", "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.6.tgz", @@ -26836,6 +29938,16 @@ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==" }, + "enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "requires": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + } + }, "entities": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", @@ -27489,6 +30601,15 @@ "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "dev": true }, + "events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "requires": { + "bare-events": "^2.7.0" + } + }, "execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -28058,6 +31179,14 @@ "invariant": "^2.2.4" } }, + "expo-local-authentication": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/expo-local-authentication/-/expo-local-authentication-14.0.1.tgz", + "integrity": "sha512-kAwUD1wEqj1fhwQgIHlP4H/JV9AcX+NO3BJwhPM2HuCFS0kgx2wvcHisnKBSTRyl8u5Jt4odzMyQkDJystwUTg==", + "requires": { + "invariant": "^2.2.4" + } + }, "expo-location": { "version": "56.0.18", "resolved": "https://registry.npmjs.org/expo-location/-/expo-location-56.0.18.tgz", @@ -28379,11 +31508,170 @@ "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==" }, + "express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "dev": true, + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true + }, + "finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true + }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true + } + } + }, + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.2.6" + }, + "dependencies": { + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + } + } + }, + "extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "requires": { + "@types/yauzl": "^2.9.1", + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "dependencies": { + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + } + } + }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, + "fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true + }, "fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -28472,6 +31760,32 @@ "resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz", "integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==" }, + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "requires": { + "pend": "~1.2.0" + } + }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + }, + "dependencies": { + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + } + } + }, "file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -28596,6 +31910,12 @@ "mime-types": "^2.1.35" } }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true + }, "freeport-async": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/freeport-async/-/freeport-async-2.0.0.tgz", @@ -28728,6 +32048,17 @@ "resolve-pkg-maps": "^1.0.0" } }, + "get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "dev": true, + "requires": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + } + }, "getenv": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/getenv/-/getenv-2.0.0.tgz", @@ -28957,6 +32288,12 @@ } } }, + "http-link-header": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/http-link-header/-/http-link-header-1.1.4.tgz", + "integrity": "sha512-xT3GPW6/ZbGuw4UvwHqErSCEjNUlwbQJuZn9/q5U4WEKfp2kENVCAlousG1zLxHeaQ/ffOHUNpWamvkbBW0eNw==", + "dev": true + }, "http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -29021,6 +32358,12 @@ "queue": "6.0.2" } }, + "image-ssim": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/image-ssim/-/image-ssim-0.2.0.tgz", + "integrity": "sha512-W7+sO6/yhxy83L0G7xR8YAc5Z5QFtYEXXRV6EaE8tuYBZJnA3gVgp3q7X7muhLZVodeb9UfvjSbwt9VJwjIYAg==", + "dev": true + }, "import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -29087,6 +32430,173 @@ "css-in-js-utils": "^3.1.0" } }, + "inquirer": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", + "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==", + "dev": true, + "requires": { + "ansi-escapes": "^3.2.0", + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^2.0.0", + "lodash": "^4.17.12", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^6.4.0", + "string-width": "^2.1.0", + "strip-ansi": "^5.1.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "dev": true + }, + "ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", + "dev": true, + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", + "dev": true, + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", + "dev": true, + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + } + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, "internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -29098,6 +32608,18 @@ "side-channel": "^1.1.0" } }, + "intl-messageformat": { + "version": "10.7.18", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.18.tgz", + "integrity": "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==", + "dev": true, + "requires": { + "@formatjs/ecma402-abstract": "2.3.6", + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/icu-messageformat-parser": "2.11.4", + "tslib": "^2.8.0" + } + }, "invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -29106,6 +32628,18 @@ "loose-envify": "^1.0.0" } }, + "ip-address": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz", + "integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==", + "dev": true + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true + }, "is-arguments": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", @@ -29344,6 +32878,12 @@ "has-tostringtag": "^1.0.2" } }, + "is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true + }, "is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", @@ -29416,6 +32956,12 @@ "which-typed-array": "^1.1.16" } }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true + }, "is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -29466,6 +33012,16 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, + "isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "dev": true, + "requires": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, "istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -29820,14 +33376,6 @@ "react-test-renderer": "19.1.0", "server-only": "^0.0.1", "stacktrace-js": "^2.0.2" - }, - "dependencies": { - "lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "dev": true - } } }, "jest-get-type": { @@ -30315,6 +33863,18 @@ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==" }, + "jpeg-js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", + "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", + "dev": true + }, + "js-library-detector": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/js-library-detector/-/js-library-detector-6.7.0.tgz", + "integrity": "sha512-c80Qupofp43y4cJ7+8TTDN/AsDwLi5oOm/plBrWI+iQt485vKXCco+yVmOwEgdo9VOdsYTuV0UlTeetVPTriXA==", + "dev": true + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -30468,6 +34028,83 @@ "type-check": "~0.4.0" } }, + "lighthouse": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/lighthouse/-/lighthouse-12.1.0.tgz", + "integrity": "sha512-PQLaNcv3tQcybnYux6T8uoS6+RNrNYvVJBbGo0kkbD4XTjesGslOXWeMkUQDK7c28nLfVZi7gYWDUsicTLglKQ==", + "dev": true, + "requires": { + "@paulirish/trace_engine": "^0.0.23", + "@sentry/node": "^6.17.4", + "axe-core": "^4.9.1", + "chrome-launcher": "^1.1.2", + "configstore": "^5.0.1", + "csp_evaluator": "1.1.1", + "devtools-protocol": "0.0.1312386", + "enquirer": "^2.3.6", + "http-link-header": "^1.1.1", + "intl-messageformat": "^10.5.3", + "jpeg-js": "^0.4.4", + "js-library-detector": "^6.7.0", + "lighthouse-logger": "^2.0.1", + "lighthouse-stack-packs": "1.12.1", + "lodash": "^4.17.21", + "lookup-closest-locale": "6.2.0", + "metaviewport-parser": "0.3.0", + "open": "^8.4.0", + "parse-cache-control": "1.0.1", + "puppeteer-core": "^22.11.1", + "robots-parser": "^3.0.1", + "semver": "^5.3.0", + "speedline-core": "^1.4.3", + "third-party-web": "^0.24.3", + "tldts-icann": "^6.1.16", + "ws": "^8.21.0", + "yargs": "^17.3.1", + "yargs-parser": "^21.0.0" + }, + "dependencies": { + "chrome-launcher": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-1.2.1.tgz", + "integrity": "sha512-qmFR5PLMzHyuNJHwOloHPAHhbaNglkfeV/xDtt5b7xiFFyU1I+AZZX0PYseMuhenJSSirgxELYIbswcoc+5H4A==", + "dev": true, + "requires": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^2.0.1" + } + }, + "lighthouse-logger": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-2.0.2.tgz", + "integrity": "sha512-vWl2+u5jgOQuZR55Z1WM0XDdrJT6mzMP8zHUct7xTlWhuQs+eV0g+QL0RQdFjT54zVmbhLCP8vIVpy1wGn/gCg==", + "dev": true, + "requires": { + "debug": "^4.4.1", + "marky": "^1.2.2" + } + }, + "open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "requires": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + } + }, + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true + } + } + }, "lighthouse-logger": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz", @@ -30492,6 +34129,12 @@ } } }, + "lighthouse-stack-packs": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/lighthouse-stack-packs/-/lighthouse-stack-packs-1.12.1.tgz", + "integrity": "sha512-i4jTmg7tvZQFwNFiwB+nCK6a7ICR68Xcwo+VIVd6Spi71vBNFUlds5HiDrSbClZdkQDON2Bhqv+KKJIo5zkPeA==", + "dev": true + }, "lightningcss": { "version": "1.27.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.27.0.tgz", @@ -30639,6 +34282,12 @@ "p-locate": "^5.0.0" } }, + "lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true + }, "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -30769,6 +34418,12 @@ } } }, + "lookup-closest-locale": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/lookup-closest-locale/-/lookup-closest-locale-6.2.0.tgz", + "integrity": "sha512-/c2kL+Vnp1jnV6K6RpDTHK3dgg0Tu2VVp+elEiJpjfS1UyY7AjOYHohRug6wT0OpoX2qFgNORndE9RqesfVxWQ==", + "dev": true + }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -30783,6 +34438,12 @@ "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", "dev": true }, + "lru_map": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz", + "integrity": "sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==", + "dev": true + }, "lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -30858,6 +34519,12 @@ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true + }, "memoize-one": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", @@ -30872,6 +34539,12 @@ "map-or-similar": "^1.5.0" } }, + "merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true + }, "merge-options": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz", @@ -30890,6 +34563,18 @@ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" }, + "metaviewport-parser": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/metaviewport-parser/-/metaviewport-parser-0.3.0.tgz", + "integrity": "sha512-EoYJ8xfjQ6kpe9VbVHvZTZHiOl4HL1Z18CrZ+qahvLXT7ZO4YTC2JMyt5FaUp9JJp6J4Ybb/z7IsCXZt86/QkQ==", + "dev": true + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true + }, "metro": { "version": "0.83.7", "resolved": "https://registry.npmjs.org/metro/-/metro-0.83.7.tgz", @@ -31605,6 +35290,12 @@ "minipass": "^7.1.2" } }, + "mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true + }, "mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -31639,6 +35330,12 @@ } } }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==", + "dev": true + }, "mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -31686,6 +35383,12 @@ "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.0.1.tgz", "integrity": "sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==" }, + "netmask": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", + "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", + "dev": true + }, "node-exports-info": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz", @@ -32140,6 +35843,60 @@ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" }, + "pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "dev": true, + "requires": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "dependencies": { + "agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true + }, + "http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "requires": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + } + }, + "https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "requires": { + "agent-base": "^7.1.2", + "debug": "4" + } + } + } + }, + "pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dev": true, + "requires": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + } + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -32149,6 +35906,12 @@ "callsites": "^3.0.0" } }, + "parse-cache-control": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", + "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==", + "dev": true + }, "parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -32233,6 +35996,12 @@ } } }, + "path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "dev": true + }, "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -32245,6 +36014,12 @@ "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", "dev": true }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true + }, "picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -32487,6 +36262,66 @@ } } }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "dev": true, + "requires": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "dependencies": { + "agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true + }, + "http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "requires": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + } + }, + "https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "requires": { + "agent-base": "^7.1.2", + "debug": "4" + } + }, + "lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true + } + } + }, "proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -32501,11 +36336,34 @@ "punycode": "^2.3.1" } }, + "pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" }, + "puppeteer-core": { + "version": "22.15.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.15.0.tgz", + "integrity": "sha512-cHArnywCiAAVXa3t4GGL2vttNxh7GqXtIYGym99egkNJ3oG//wL9LkvO4WE8W1TJe95t1F1ocu9X4xWaGsOKOA==", + "dev": true, + "requires": { + "@puppeteer/browsers": "2.3.0", + "chromium-bidi": "0.6.3", + "debug": "^4.3.6", + "devtools-protocol": "0.0.1312386", + "ws": "^8.21.0" + } + }, "pure-rand": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", @@ -32517,6 +36375,16 @@ "resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.11.0.tgz", "integrity": "sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ==" }, + "qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "dev": true, + "requires": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + } + }, "query-string": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", @@ -32552,6 +36420,29 @@ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" }, + "raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "dev": true, + "requires": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "dependencies": { + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + } + } + }, "rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -33137,6 +37028,12 @@ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, "require-package-name": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/require-package-name/-/require-package-name-2.0.1.tgz", @@ -33289,12 +37186,24 @@ } } }, + "robots-parser": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/robots-parser/-/robots-parser-3.0.1.tgz", + "integrity": "sha512-s+pyvQeIKIZ0dx5iJiQk1tPLJAWln39+MI5jtM8wnyws+G5azk+dMnMX0qfbqNetKKNgcWWOdi0sfm+FbQbgdQ==", + "dev": true + }, "run-applescript": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", "dev": true }, + "run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true + }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -33303,6 +37212,23 @@ "queue-microtask": "^1.2.2" } }, + "rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, "safe-array-concat": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", @@ -33460,6 +37386,12 @@ "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz", "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==" }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true + }, "set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -33645,6 +37577,12 @@ "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.9.tgz", "integrity": "sha512-vZ7rfeehZui7wQs438JXBckYLkIIdfHOXsaVEUMyS5fHo1483l1bMdo0EDSWYclY0yZKFOipDy4KHuKs6ssvdg==" }, + "smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true + }, "socket.io-client": { "version": "4.8.3", "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", @@ -33665,6 +37603,35 @@ "debug": "~4.4.1" } }, + "socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "dev": true, + "requires": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + } + }, + "socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "requires": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "dependencies": { + "agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true + } + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -33685,6 +37652,17 @@ "source-map": "^0.6.0" } }, + "speedline-core": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/speedline-core/-/speedline-core-1.4.3.tgz", + "integrity": "sha512-DI7/OuAUD+GMpR6dmu8lliO2Wg5zfeh+/xsdyJZCzd8o5JgFUjCeLsBDuZjIQJdwXS3J0L/uZYrELKYqx+PXog==", + "dev": true, + "requires": { + "@types/node": "*", + "image-ssim": "^0.2.0", + "jpeg-js": "^0.4.1" + } + }, "split-on-first": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", @@ -33854,6 +37832,17 @@ "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz", "integrity": "sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==" }, + "streamx": { + "version": "2.28.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.1.tgz", + "integrity": "sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==", + "dev": true, + "requires": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, "strict-uri-encode": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", @@ -34132,6 +38121,39 @@ } } }, + "tar-fs": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", + "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", + "dev": true, + "requires": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0", + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + } + }, + "tar-stream": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.1.tgz", + "integrity": "sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==", + "dev": true, + "requires": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "requires": { + "streamx": "^2.12.5" + } + }, "terminal-link": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", @@ -34201,6 +38223,15 @@ } } }, + "text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "dev": true, + "requires": { + "b4a": "^1.6.4" + } + }, "thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -34217,11 +38248,23 @@ "thenify": ">= 3.1.0 < 4" } }, + "third-party-web": { + "version": "0.24.5", + "resolved": "https://registry.npmjs.org/third-party-web/-/third-party-web-0.24.5.tgz", + "integrity": "sha512-1rUOdMYpNTRajgk1F7CmHD26oA6rTKekBjHay854J6OkPXeNyPcR54rhWDaamlWyi9t2wAVPQESdedBhucmOLA==", + "dev": true + }, "throat": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==" }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, "tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -34274,6 +38317,21 @@ "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", "dev": true }, + "tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true + }, + "tldts-icann": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-icann/-/tldts-icann-6.1.86.tgz", + "integrity": "sha512-NFxmRT2lAEMcCOBgeZ0NuM0zsK/xgmNajnY6n4S1mwAKocft2s2ise1O3nQxrH3c+uY6hgHUV9GGNVp7tUE4Sg==", + "dev": true, + "requires": { + "tldts-core": "^6.1.86" + } + }, "tmcp": { "version": "1.19.4", "resolved": "https://registry.npmjs.org/tmcp/-/tmcp-1.19.4.tgz", @@ -34287,6 +38345,12 @@ "valibot": "^1.1.0" } }, + "tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true + }, "tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -34326,6 +38390,12 @@ "punycode": "^2.1.1" } }, + "tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true + }, "ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -34407,6 +38477,16 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==" }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, "typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -34460,6 +38540,15 @@ "reflect.getprototypeof": "^1.0.10" } }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "requires": { + "is-typedarray": "^1.0.0" + } + }, "typescript": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", @@ -34483,6 +38572,16 @@ "which-boxed-primitive": "^1.1.1" } }, + "unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dev": true, + "requires": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, "undici": { "version": "6.27.0", "resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz", @@ -34517,6 +38616,15 @@ "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==" }, + "unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dev": true, + "requires": { + "crypto-random-string": "^2.0.0" + } + }, "universalify": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", @@ -34593,6 +38701,12 @@ "requires-port": "^1.0.0" } }, + "urlpattern-polyfill": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", + "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==", + "dev": true + }, "use-callback-ref": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", @@ -34916,6 +39030,12 @@ "is-weakset": "^2.0.3" } }, + "which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "dev": true + }, "which-typed-array": { "version": "1.1.22", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", @@ -35041,6 +39161,12 @@ "uuid": "^11.0.3" } }, + "xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "dev": true + }, "xml-name-validator": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", @@ -35135,6 +39261,16 @@ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" }, + "yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "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 6c26b322..b9b2d1a6 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,6 @@ "expo-build-properties": "~1.0.10", "expo-constants": "~18.0.13", "expo-crypto": "~14.0.1", - "expo-local-authentication": "~14.0.1", "expo-device": "~8.0.10", "expo-document-picker": "~14.0.8", "expo-file-system": "~19.0.23", @@ -83,6 +82,7 @@ "expo-keep-awake": "~15.0.8", "expo-linear-gradient": "^15.0.8", "expo-linking": "~8.0.11", + "expo-local-authentication": "~14.0.1", "expo-location": "^56.0.18", "expo-network": "~8.0.8", "expo-notifications": "~0.32.16", @@ -122,6 +122,7 @@ "@babel/core": "^7.29.0", "@gorhom/bottom-sheet": "^5.2.14", "@jest/globals": "^29.7.0", + "@lhci/cli": "^0.14.0", "@react-native-community/datetimepicker": "^9.1.0", "@react-native-community/slider": "^5.2.0", "@storybook/addon-ondevice-actions": "^10.4.4", @@ -196,7 +197,6 @@ "expo-asset", "expo-speech-recognition", "prettier-plugin-tailwindcss", - "@lhci/cli", "@testing-library/jest-native", "@types/babel__core", "@types/babel__generator", diff --git a/src/services/webVitals.ts b/src/services/webVitals.ts index bb6206b7..6621651e 100644 --- a/src/services/webVitals.ts +++ b/src/services/webVitals.ts @@ -2,21 +2,25 @@ import { mobileAnalyticsService } from './mobileAnalytics'; import { appLogger } from '../utils/logger'; import { AnalyticsEvent, PerformanceMetric } from '../utils/trackingEvents'; -import type { Metric } from 'web-vitals'; +import type { Metric, MetricType } from 'web-vitals'; /** * Google's recommended thresholds for Core Web Vitals. * "good" = passes, "needsImprovement" = warning, above = poor. + * + * Note: FID (First Input Delay) was deprecated by web-vitals v5 in favour of + * INP (Interaction to Next Paint). The thresholds below reflect the latest + * Google recommendations. */ export const WEB_VITALS_THRESHOLDS: Record< - PerformanceMetric.LCP | PerformanceMetric.FID | PerformanceMetric.CLS | PerformanceMetric.FCP | PerformanceMetric.TTFB, + PerformanceMetric.LCP | PerformanceMetric.INP | PerformanceMetric.CLS | PerformanceMetric.FCP | PerformanceMetric.TTFB, { good: number; needsImprovement: number } > = { - [PerformanceMetric.LCP]: { good: 2500, needsImprovement: 4000 }, - [PerformanceMetric.FID]: { good: 100, needsImprovement: 300 }, - [PerformanceMetric.CLS]: { good: 0.1, needsImprovement: 0.25 }, - [PerformanceMetric.FCP]: { good: 1800, needsImprovement: 3000 }, - [PerformanceMetric.TTFB]: { good: 800, needsImprovement: 1800 }, + [PerformanceMetric.LCP]: { good: 2500, needsImprovement: 4000 }, + [PerformanceMetric.INP]: { good: 200, needsImprovement: 500 }, + [PerformanceMetric.CLS]: { good: 0.1, needsImprovement: 0.25 }, + [PerformanceMetric.FCP]: { good: 1800, needsImprovement: 3000 }, + [PerformanceMetric.TTFB]: { good: 800, needsImprovement: 1800 }, }; export type VitalRating = 'good' | 'needs-improvement' | 'poor'; @@ -32,10 +36,15 @@ export interface VitalReport { /** Stored baselines for regression detection (keyed by metric name). */ const baselines = new Map(); -function getRating( - metric: PerformanceMetric.LCP | PerformanceMetric.FID | PerformanceMetric.CLS | PerformanceMetric.FCP | PerformanceMetric.TTFB, - value: number -): VitalRating { +/** Type-safe subset of PerformanceMetric used for rating and thresholds. */ +type RatedMetric = + | PerformanceMetric.LCP + | PerformanceMetric.INP + | PerformanceMetric.CLS + | PerformanceMetric.FCP + | PerformanceMetric.TTFB; + +function getRating(metric: RatedMetric, value: number): VitalRating { const { good, needsImprovement } = WEB_VITALS_THRESHOLDS[metric]; if (value <= good) return 'good'; if (value <= needsImprovement) return 'needs-improvement'; @@ -44,20 +53,17 @@ function getRating( function toAnalyticsEvent(metric: PerformanceMetric): AnalyticsEvent { const map: Record = { - [PerformanceMetric.LCP]: AnalyticsEvent.WEB_VITALS_LCP, - [PerformanceMetric.FID]: AnalyticsEvent.WEB_VITALS_FID, - [PerformanceMetric.CLS]: AnalyticsEvent.WEB_VITALS_CLS, - [PerformanceMetric.FCP]: AnalyticsEvent.WEB_VITALS_FCP, + [PerformanceMetric.LCP]: AnalyticsEvent.WEB_VITALS_LCP, + [PerformanceMetric.INP]: AnalyticsEvent.WEB_VITALS_INP, + [PerformanceMetric.CLS]: AnalyticsEvent.WEB_VITALS_CLS, + [PerformanceMetric.FCP]: AnalyticsEvent.WEB_VITALS_FCP, [PerformanceMetric.TTFB]: AnalyticsEvent.WEB_VITALS_TTFB, }; return map[metric] ?? AnalyticsEvent.PERFORMANCE_METRIC; } -function handleMetric(perfMetric: PerformanceMetric, raw: Metric): void { - const rating = getRating( - perfMetric as PerformanceMetric.LCP | PerformanceMetric.FID | PerformanceMetric.CLS | PerformanceMetric.FCP | PerformanceMetric.TTFB, - raw.value - ); +function handleMetric(perfMetric: RatedMetric, raw: MetricType): void { + const rating = getRating(perfMetric, raw.value); const report: VitalReport = { name: perfMetric, @@ -97,8 +103,6 @@ function handleMetric(perfMetric: PerformanceMetric, raw: Metric): void { // First reading becomes the baseline baselines.set(perfMetric, raw.value); } - - return report as unknown as void; // typed void for callback compatibility } /** @@ -110,11 +114,11 @@ export function init(): void { // Dynamic import keeps the web-vitals bundle out of the critical path and // avoids hard failures in environments where the APIs don't exist. import('web-vitals') - .then(({ onLCP, onFID, onCLS, onFCP, onTTFB }) => { - onLCP((m) => handleMetric(PerformanceMetric.LCP, m)); - onFID((m) => handleMetric(PerformanceMetric.FID, m)); - onCLS((m) => handleMetric(PerformanceMetric.CLS, m)); - onFCP((m) => handleMetric(PerformanceMetric.FCP, m)); + .then(({ onLCP, onINP, onCLS, onFCP, onTTFB }) => { + onLCP((m) => handleMetric(PerformanceMetric.LCP, m)); + onINP((m) => handleMetric(PerformanceMetric.INP, m)); + onCLS((m) => handleMetric(PerformanceMetric.CLS, m)); + onFCP((m) => handleMetric(PerformanceMetric.FCP, m)); onTTFB((m) => handleMetric(PerformanceMetric.TTFB, m)); appLogger.infoSync('[WebVitals] Monitoring initialised'); }) diff --git a/src/utils/trackingEvents.ts b/src/utils/trackingEvents.ts index 6c3d085c..859edbf4 100644 --- a/src/utils/trackingEvents.ts +++ b/src/utils/trackingEvents.ts @@ -67,7 +67,7 @@ export enum AnalyticsEvent { // Core Web Vitals WEB_VITALS_LCP = 'web_vitals_lcp', - WEB_VITALS_FID = 'web_vitals_fid', + WEB_VITALS_INP = 'web_vitals_inp', WEB_VITALS_CLS = 'web_vitals_cls', WEB_VITALS_FCP = 'web_vitals_fcp', WEB_VITALS_TTFB = 'web_vitals_ttfb', @@ -113,7 +113,7 @@ export enum PerformanceMetric { // Core Web Vitals LCP = 'lcp', - FID = 'fid', + INP = 'inp', CLS = 'cls', FCP = 'fcp', TTFB = 'ttfb', From 76ec573045dd0e29ef9d5e4b9da06175aa81ef39 Mon Sep 17 00:00:00 2001 From: ustaxs Date: Thu, 27 Aug 2026 09:01:57 +0100 Subject: [PATCH 2/6] perf: expand performance budgets and block baseline regeneration from fabricated data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Expand performance-budget.json to cover startup, bundle size, frame rate, API latency (p50/p95/p99), memory, and Lighthouse budgets with rationale - Mark performance-baseline.json as fabricated until real measurements replace Math.random() values in MemoryAnalyzer, RuntimeAnalyzer, NetworkAnalyzer - checkPerfRegression.js now fails when a metric is missing instead of skipping - updatePerfBaseline.js refuses to overwrite a fabricated baseline - checkBundleSize.js and checkApiPerf.js use the expanded budget structure - measureStartupTime.js reads p95_ms from startupTime budget - Rewrite docs/PERFORMANCE_THRESHOLDS.md with per-metric budgets and rationale ๐Ÿค– Generated with Codebuff Co-Authored-By: Codebuff --- docs/PERFORMANCE_THRESHOLDS.md | 101 +++++++++++++++++++++++++-------- performance-baseline.json | 33 ++++++++--- performance-budget.json | 36 +++++++++++- scripts/checkApiPerf.js | 58 ++++++++++++++----- scripts/checkBundleSize.js | 12 ++-- scripts/checkPerfRegression.js | 28 ++++----- scripts/measureStartupTime.js | 2 +- scripts/updatePerfBaseline.js | 25 +++++++- 8 files changed, 223 insertions(+), 72 deletions(-) diff --git a/docs/PERFORMANCE_THRESHOLDS.md b/docs/PERFORMANCE_THRESHOLDS.md index c240ab28..ec83a945 100644 --- a/docs/PERFORMANCE_THRESHOLDS.md +++ b/docs/PERFORMANCE_THRESHOLDS.md @@ -5,26 +5,66 @@ Any metric that worsens by **more than 5%** vs the stored baseline will fail the --- -## Thresholds - -| Metric | Budget | Regression Gate | -| --------------------- | -------- | ------------------------ | -| Bundle size (Android) | 2.5 MB | >5% increase vs baseline | -| Bundle size (iOS) | 2.5 MB | >5% increase vs baseline | -| Bundle size (total) | 5 MB | >5% increase vs baseline | -| Startup time p95 | 2 000 ms | >5% increase vs baseline | -| API latency p95 | 1 000 ms | >5% increase vs baseline | -| API latency p99 | 2 000 ms | >5% increase vs baseline | -| API error rate | < 1% | absolute (k6 threshold) | +## Absolute Budgets Absolute budgets are defined in [`performance-budget.json`](../performance-budget.json). -The regression baseline is stored in [`performance-baseline.json`](../performance-baseline.json). +Every gated metric must have a documented budget below. + +### Bundle Size + +| Metric | Budget | Rationale | +|---|---|---| +| Android bundle | 2.5 MB | Keeps install size under Play Store's "large app" warning threshold (50 MB download). Covers JS + assets for a mid-complexity educational app. | +| iOS bundle | 2.5 MB | Mirrors Android for parity. iOS has higher memory overhead, so the bundle should be conservative. | +| Total (all platforms) | 5 MB | Aggregate cap across Android + iOS ensures neither platform silently grows while the other stays flat. | + +### Startup Time + +| Metric | Budget | Rationale | +|---|---|---| +| p50 | 1 000 ms | Median user should see the splash screen dismissed within 1 s. | +| p95 | 2 000 ms | Worst-case 95th-percentile budget based on Google's "Time to Interactive" guidance for mobile web. | + +### Frame Rate + +| Metric | Budget | Rationale | +|---|---|---| +| Min FPS | โ‰ฅ 55 fps | Targeting smooth 60 fps with a small margin. Drops below 55 fps cause visible jank. | +| Max dropped frames / session | โ‰ค 5 | More than 5 dropped frames per session is perceptible and correlates with poor App Store reviews. | + +### API Latency + +| Metric | Budget | Rationale | +|---|---|---| +| p50 | 300 ms | Median response should be near-instant; users perceive > 300 ms as sluggish. | +| p95 | 1 000 ms | The 95th percentile covers regional CDN variations and cold-start penalties. | +| p99 | 2 000 ms | Upper bound โ€” the slowest 1 % of requests should still complete within 2 s. | + +### Memory + +| Metric | Budget | Rationale | +|---|---|---| +| Max heap | 128 MB | Exceeding 128 MB heap triggers GC pauses > 50 ms on mid-range Android devices. | +| Max native | 80 MB | Native memory (images, fonts, video) should stay under 80 MB to avoid OOM kills on low-RAM devices. | + +### Lighthouse + +| Metric | Budget | Rationale | +|---|---|---| +| Performance score | โ‰ฅ 50 | Minimum passing score for CI. Higher scores require further optimisation work. | +| FCP | โ‰ค 3 000 ms | First Contentful Paint โ€” users see initial content within 3 s. | +| LCP | โ‰ค 4 000 ms | Largest Contentful Paint โ€” main content is visible within 4 s. | +| CLS | โ‰ค 0.25 | Cumulative Layout Shift โ€” below the "needs improvement" threshold. | +| TBT | โ‰ค 3 000 ms | Total Blocking Time โ€” main thread is responsive. | +| Speed Index | โ‰ค 4 000 ms | Visual completeness within 4 s. | +| TTI | โ‰ค 5 000 ms | Time to Interactive โ€” app is fully interactive within 5 s. | --- -## CI Workflow +## Regression Gate -The workflow (`.github/workflows/performance-regression.yml`) runs four jobs: +The CI workflow (`.github/workflows/performance-regression.yml`) runs four jobs and a +consolidated regression gate: ### 1. `bundle-size` @@ -43,8 +83,8 @@ The workflow (`.github/workflows/performance-regression.yml`) runs four jobs: ### 3. `api-latency` - Installs [k6](https://k6.io) and runs `scripts/k6-api-benchmark.js` -- 3-stage load: ramp 0โ†’5 VUs (10s), hold 10 VUs (20s), ramp down (10s) -- k6 built-in thresholds: `p(95)<1000ms`, `p(99)<2000ms`, `error_rate<1%` +- 3-stage load: ramp 0โ†’5 VUs (10 s), hold 10 VUs (20 s), ramp down (10 s) +- k6 built-in thresholds: `p(95)<1000 ms`, `p(99)<2000 ms`, `error_rate<1%` - Compares p95 against the cached baseline - Fails if p95 API latency grew by >5% @@ -52,8 +92,10 @@ The workflow (`.github/workflows/performance-regression.yml`) runs four jobs: - Downloads all three reports - Runs `scripts/checkPerfRegression.js` for a consolidated view +- **Missing metrics fail the check** โ€” a null baseline or null current value + is treated as a regression, not silently skipped - Posts a summary table as a PR comment (updates existing comment on re-runs) -- Fails the gate if any metric regressed by >5% +- Fails the gate if any metric regressed by >5% or is missing --- @@ -61,10 +103,21 @@ The workflow (`.github/workflows/performance-regression.yml`) runs four jobs: Baselines are stored in two places: -| Store | Purpose | -| --------------------------------------------- | ---------------------------------------------------------- | -| `performance-baseline.json` (committed) | Human-readable reference; used by `checkPerfRegression.js` | -| GitHub Actions cache (`perf-*-baseline-main`) | Per-job comparison; updated on every `main` push | +| Store | Purpose | +|---|---| +| `performance-baseline.json` (committed) | Human-readable reference; used by `checkPerfRegression.js` | +| GitHub Actions cache (`perf-*-baseline-main`) | Per-job comparison; updated on every `main` push | + +### Baseline regeneration guard + +`npm run perf:update-baseline` will **refuse to run** if the current baseline +contains fabricated data (`"fabricated": true`). The audit analyzers that use +`Math.random()` to generate fake metrics must be replaced with real measurements +before the baseline chain is trusted: + +- `src/audit/analyzers/MemoryAnalyzer.ts` โ€” fabricated `avgRenderTime`, `renders`, `droppedFrames` +- `src/audit/analyzers/RuntimeAnalyzer.ts` โ€” fabricated event loop lag +- `src/audit/analyzers/NetworkAnalyzer.ts` โ€” fabricated `avgLatency`, `errorRate`, `requests`, `dataSize` ### Updating the baseline @@ -95,13 +148,13 @@ The GitHub Actions cache baselines update automatically on every successful `mai ```bash # Startup time benchmark (10 iterations) -npm run perf:startup +node scripts/measureStartupTime.js # API latency (requires k6 installed: https://k6.io/docs/get-started/installation/) k6 run scripts/k6-api-benchmark.js # Consolidated regression check (reads reports/ directory) -npm run perf:regression +node scripts/checkPerfRegression.js # Update baseline from latest reports npm run perf:update-baseline @@ -114,7 +167,7 @@ npm run perf:update-baseline The regression threshold defaults to **5%**. To change it: - **CI**: set the `REGRESSION_THRESHOLD` env var in the workflow step -- **Local**: `REGRESSION_THRESHOLD=10 npm run perf:regression` +- **Local**: `REGRESSION_THRESHOLD=10 node scripts/checkPerfRegression.js` --- diff --git a/performance-baseline.json b/performance-baseline.json index 79562a98..ed22a1ed 100644 --- a/performance-baseline.json +++ b/performance-baseline.json @@ -1,19 +1,34 @@ { - "_comment": "Performance baseline. Update with: npm run perf:update-baseline", + "_comment": "Performance baseline โ€” contains ONLY measured values. Update with: npm run perf:update-baseline", + "_warning": "Baseline regeneration is blocked until Math.random() fabrications in MemoryAnalyzer, RuntimeAnalyzer, and NetworkAnalyzer are replaced with real measurements.", "version": "1.0.0", "timestamp": "2026-06-06T00:00:00.000Z", + "fabricated": true, "bundleSize": { - "android_bytes": 4000000, - "ios_bytes": 3800000, - "total_bytes": 7800000 + "android_bytes": null, + "ios_bytes": null, + "total_bytes": null, + "_note": "Awaiting real bundle measurement โ€” see performance-regression.yml bundle-size job" }, "startupTime": { - "p50_ms": 50, - "p95_ms": 200 + "p50_ms": null, + "p95_ms": null, + "_note": "Awaiting real startup measurement โ€” see performance-regression.yml startup-time job" }, "apiLatency": { - "p50_ms": 200, - "p95_ms": 600, - "p99_ms": 900 + "p50_ms": null, + "p95_ms": null, + "p99_ms": null, + "_note": "Awaiting real API latency measurement โ€” see performance-regression.yml api-latency job" + }, + "frameRate": { + "min_fps": null, + "maxDroppedFrames": null, + "_note": "Awaiting real frame rate measurement" + }, + "memory": { + "maxHeapMB": null, + "maxNativeMB": null, + "_note": "Awaiting real memory measurement" } } diff --git a/performance-budget.json b/performance-budget.json index af71ba62..7829a778 100644 --- a/performance-budget.json +++ b/performance-budget.json @@ -1,5 +1,35 @@ { - "bundleSize": 52428800, - "tti": 2000, - "apiResponse": 1000 + "_comment": "Absolute performance budgets for TeachLink. See docs/PERFORMANCE_THRESHOLDS.md for rationale.", + "version": "1.0.0", + "bundleSize": { + "android_bytes": 2621440, + "ios_bytes": 2621440, + "total_bytes": 5242880 + }, + "startupTime": { + "p50_ms": 1000, + "p95_ms": 2000 + }, + "frameRate": { + "min_fps": 55, + "maxDroppedFrames": 5 + }, + "apiLatency": { + "p50_ms": 300, + "p95_ms": 1000, + "p99_ms": 2000 + }, + "memory": { + "maxHeapMB": 128, + "maxNativeMB": 80 + }, + "lighthouse": { + "minPerformanceScore": 50, + "maxFCP": 3000, + "maxLCP": 4000, + "maxCLS": 0.25, + "maxTBT": 3000, + "maxSI": 4000, + "maxTTI": 5000 + } } diff --git a/scripts/checkApiPerf.js b/scripts/checkApiPerf.js index 018ddfe5..fb70569f 100644 --- a/scripts/checkApiPerf.js +++ b/scripts/checkApiPerf.js @@ -3,30 +3,58 @@ const axios = require('axios'); const budgetConfig = require('../performance-budget.json'); const API_URL = process.env.API_URL || 'https://jsonplaceholder.typicode.com/posts'; +const apiBudget = budgetConfig.apiLatency || {}; +const p50Budget = apiBudget.p50_ms || 300; +const p95Budget = apiBudget.p95_ms || 1000; async function testApiPerformance() { - try { - const start = Date.now(); + const samples = []; + const SAMPLES = 5; + + console.log('API Performance Report'); + console.log('========================'); + + for (let i = 0; i < SAMPLES; i++) { + try { + const start = Date.now(); + await axios.get(API_URL); + const duration = Date.now() - start; + samples.push(duration); + console.log(` Run ${i + 1}/${SAMPLES}: ${duration} ms`); + } catch (error) { + console.error(` Run ${i + 1}/${SAMPLES}: FAILED - ${error.message}`); + process.exit(1); + } + } - await axios.get(API_URL); + const sorted = [...samples].sort((a, b) => a - b); + const avg = Math.round(samples.reduce((s, v) => s + v, 0) / samples.length); + const p50 = sorted[Math.floor(sorted.length * 0.5)]; + const p95 = sorted[Math.floor(sorted.length * 0.95)] || sorted[sorted.length - 1]; - const duration = Date.now() - start; + console.log(''); + console.log(` Average: ${avg} ms`); + console.log(` p50: ${p50} ms (budget: ${p50Budget} ms)`); + console.log(` p95: ${p95} ms (budget: ${p95Budget} ms)`); + console.log(''); - console.log('API Performance Report'); - console.log('------------------------'); - console.log(`Budget: ${budgetConfig.apiResponse} ms`); - console.log(`Actual: ${duration} ms`); + let failed = false; - if (duration > budgetConfig.apiResponse) { - console.error('API response exceeded budget'); - process.exit(1); - } + if (p50 > p50Budget) { + console.error(`โŒ API p50 (${p50} ms) exceeded budget (${p50Budget} ms)`); + failed = true; + } + + if (p95 > p95Budget) { + console.error(`โŒ API p95 (${p95} ms) exceeded budget (${p95Budget} ms)`); + failed = true; + } - console.log('API performance passed'); - } catch (error) { - console.error(error); + if (failed) { process.exit(1); } + + console.log('โœ… API performance passed'); } testApiPerformance(); diff --git a/scripts/checkBundleSize.js b/scripts/checkBundleSize.js index 493ef429..ea4a65a3 100644 --- a/scripts/checkBundleSize.js +++ b/scripts/checkBundleSize.js @@ -1,7 +1,6 @@ const fs = require('fs'); const path = require('path'); - const budget = require('../performance-budget.json'); const possibleBuilds = ['dist', 'build', '.next']; @@ -40,13 +39,14 @@ function getSize(dir) { } const actual = getSize(buildPath); +const totalBudget = budget.bundleSize?.total_bytes || 52428800; -console.log('Bundle size:', actual); -console.log('Budget:', budget.bundleSize); +console.log('Bundle size:', actual, 'bytes'); +console.log('Budget:', totalBudget, 'bytes'); -if (actual > budget.bundleSize) { - console.error('โŒ Bundle too large'); +if (actual > totalBudget) { + console.error(`โŒ Bundle too large: ${actual} > ${totalBudget} bytes`); process.exit(1); } -console.log('โœ… Bundle OK'); +console.log(`โœ… Bundle OK (${((actual / totalBudget) * 100).toFixed(1)}% of budget)`); diff --git a/scripts/checkPerfRegression.js b/scripts/checkPerfRegression.js index 6f974c10..b5f6f068 100644 --- a/scripts/checkPerfRegression.js +++ b/scripts/checkPerfRegression.js @@ -42,8 +42,16 @@ function regressionPct(baseline, current) { } function check(label, baselineVal, currentVal, threshold) { - if (baselineVal == null || currentVal == null) { - return { label, status: 'skip', reason: 'missing data' }; + // Fail loudly when a metric is missing rather than silently skipping. + // Missing metrics hide regressions and erode trust in the regression gate. + if (baselineVal == null && currentVal == null) { + return { label, status: 'fail', reason: 'both baseline and current are missing' }; + } + if (baselineVal == null) { + return { label, status: 'fail', reason: `baseline missing for ${label} โ€” cannot gate without a known-good value` }; + } + if (currentVal == null) { + return { label, status: 'fail', reason: `current measurement missing for ${label}` }; } const pct = regressionPct(baselineVal, currentVal); const regressed = pct > threshold; @@ -107,7 +115,6 @@ function main() { const failures = checks.filter(c => c.status === 'fail'); const passes = checks.filter(c => c.status === 'pass'); - const skipped = checks.filter(c => c.status === 'skip'); const report = { timestamp: new Date().toISOString(), @@ -116,7 +123,6 @@ function main() { total: checks.length, passed: passes.length, failed: failures.length, - skipped: skipped.length, }, checks, }; @@ -132,22 +138,18 @@ function main() { console.log('โ”€'.repeat(78)); for (const c of checks) { - if (c.status === 'skip') { - console.log( - `${c.label.padEnd(30)} ${'โ€”'.padStart(12)} ${'โ€”'.padStart(12)} ${'โ€”'.padStart(10)} ${'SKIP'.padStart(8)}` - ); - continue; - } - const changeStr = `${c.change_pct > 0 ? '+' : ''}${c.change_pct}%`; + const changeStr = c.reason + ? c.reason + : `${c.change_pct > 0 ? '+' : ''}${c.change_pct}%`; const statusStr = c.status === 'pass' ? 'โœ… PASS' : 'โŒ FAIL'; console.log( - `${c.label.padEnd(30)} ${String(c.baseline).padStart(12)} ${String(c.current).padStart(12)} ${changeStr.padStart(10)} ${statusStr.padStart(8)}` + `${c.label.padEnd(30)} ${String(c.baseline ?? 'โ€”').padStart(12)} ${String(c.current ?? 'โ€”').padStart(12)} ${changeStr.padStart(20)} ${statusStr.padStart(8)}` ); } console.log('โ”€'.repeat(78)); console.log( - `\n Passed: ${passes.length} Failed: ${failures.length} Skipped: ${skipped.length}\n` + `\n Passed: ${passes.length} Failed: ${failures.length}\n` ); if (failures.length > 0) { diff --git a/scripts/measureStartupTime.js b/scripts/measureStartupTime.js index e82275e4..dba4b708 100644 --- a/scripts/measureStartupTime.js +++ b/scripts/measureStartupTime.js @@ -27,7 +27,7 @@ const BUDGET_MS = parseInt( process.env.STARTUP_BUDGET_MS || (() => { try { - return require('../performance-budget.json').tti; + return require('../performance-budget.json').startupTime?.p95_ms ?? 2000; } catch { return 2000; } diff --git a/scripts/updatePerfBaseline.js b/scripts/updatePerfBaseline.js index c67bc010..07266126 100644 --- a/scripts/updatePerfBaseline.js +++ b/scripts/updatePerfBaseline.js @@ -26,16 +26,39 @@ function readJSON(filePath) { } function main() { + // Block baseline regeneration if the current baseline still contains + // fabricated data (Math.random() values). The existing baseline must + // be replaced with measured values before the chain can proceed. + const existingBaseline = readJSON(path.join(ROOT, 'performance-baseline.json')); + if (existingBaseline && existingBaseline.fabricated === true) { + console.error('โŒ Cannot update baseline: the current baseline contains fabricated data.'); + console.error(' All Math.random() fabrications in the audit analyzers must be'); + console.error(' replaced with real measurements before the baseline can be updated.'); + console.error(''); + console.error(' See: src/audit/analyzers/MemoryAnalyzer.ts'); + console.error(' src/audit/analyzers/RuntimeAnalyzer.ts'); + console.error(' src/audit/analyzers/NetworkAnalyzer.ts'); + process.exit(1); + } + const startup = readJSON(path.join(REPORTS, 'startup-benchmark.json')); const k6 = readJSON(path.join(REPORTS, 'k6-summary.json')); const bundle = readJSON(path.join(REPORTS, 'bundle-sizes.json')); - const existing = readJSON(path.join(ROOT, 'performance-baseline.json')) || {}; + // Require at least one real measurement to proceed + if (!startup && !k6 && !bundle) { + console.error('โŒ No benchmark reports found in reports/ directory.'); + console.error(' Run benchmarks first before updating the baseline.'); + process.exit(1); + } + + const existing = existingBaseline || {}; const baseline = { _comment: 'Performance baseline. Update with: npm run perf:update-baseline', version: '1.0.0', timestamp: new Date().toISOString(), + fabricated: false, bundleSize: bundle ? { android_bytes: bundle.android_bytes ?? existing.bundleSize?.android_bytes, From 9f6f1e8fbcf21ff3d1da8f45c6477983ffed65d0 Mon Sep 17 00:00:00 2001 From: ustaxs Date: Thu, 27 Aug 2026 09:03:47 +0100 Subject: [PATCH 3/6] fix: pin fonttools to exact version and include requirements.txt in cache key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Pin fonttools==4.62.0 in requirements.txt for reproducible font subsetting - Change CI step from `pip install fonttools` to `pip install -r requirements.txt` - Add requirements.txt to the font cache key so a tool upgrade invalidates the cache - Ensures font subsetting output is byte-identical across builds ๐Ÿค– Generated with Codebuff Co-Authored-By: Codebuff --- .github/workflows/ci.yml | 4 ++-- requirements.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index addb62fc..eff25b5d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,7 +71,7 @@ jobs: ${{ runner.os }}-pip- - name: Install subsetting dependencies - run: pip install fonttools + run: pip install -r requirements.txt - name: Setup Node.js uses: actions/setup-node@v4 @@ -89,7 +89,7 @@ jobs: path: | assets/fonts/*.ttf !assets/fonts/original/*.ttf - key: ${{ runner.os }}-fonts-${{ hashFiles('assets/fonts/original/*.ttf', 'scripts/subset-fonts.py') }} + key: ${{ runner.os }}-fonts-${{ hashFiles('assets/fonts/original/*.ttf', 'scripts/subset-fonts.py', 'requirements.txt') }} restore-keys: | ${{ runner.os }}-fonts- diff --git a/requirements.txt b/requirements.txt index ff44a1d2..6ffd19b4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -fonttools>=4.57.0 +fonttools==4.62.0 From dbe1ec8058b9608ec26baefba25579957b32f36e Mon Sep 17 00:00:00 2001 From: ustaxs Date: Thu, 27 Aug 2026 09:05:15 +0100 Subject: [PATCH 4/6] refactor: consolidate font-subsetting to single Python script and document pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Delete scripts/subset-fonts.js (unused, hardcoded character sets, outputs WOFF2 incompatible with React Native) โ€” CI depends on scripts/subset-fonts.py which dynamically scans source files for actual character usage - Collapse subset-fonts and fonts:subset npm scripts to both invoke the Python script - Add docs/FONT_PIPELINE.md documenting the three-stage pipeline: subsetting โ†’ analysis โ†’ runtime loading, including CRITICAL_FONTS / SECONDARY_FONTS relationship to subset output ๐Ÿค– Generated with Codebuff Co-Authored-By: Codebuff --- docs/FONT_PIPELINE.md | 183 ++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- scripts/subset-fonts.js | 158 ---------------------------------- 3 files changed, 184 insertions(+), 159 deletions(-) create mode 100644 docs/FONT_PIPELINE.md delete mode 100644 scripts/subset-fonts.js diff --git a/docs/FONT_PIPELINE.md b/docs/FONT_PIPELINE.md new file mode 100644 index 00000000..44cbf39f --- /dev/null +++ b/docs/FONT_PIPELINE.md @@ -0,0 +1,183 @@ +# Font Pipeline + +TeachLink uses a three-stage font pipeline: **subsetting**, **analysis**, and **runtime loading**. + +--- + +## Overview + +``` +assets/fonts/original/*.ttf โ† Source fonts (checked into repo) + โ”‚ + โ–ผ +scripts/subset-fonts.py โ† Stage 1: Character-level subsetting + โ”‚ + โ–ผ +assets/fonts/*.ttf โ† Subsetted fonts (output, loaded at runtime) + โ”‚ + โ”œโ”€โ”€โ–บ src/services/fontService.ts โ† Stage 3: Runtime loading + โ”‚ +scripts/analyze-fonts.js โ† Stage 2: Character usage analysis (supplementary) +``` + +--- + +## Stage 1: Subsetting (`scripts/subset-fonts.py`) + +This is the **canonical** font subsetting script used by CI. + +### What it does + +1. Scans all `.ts`, `.tsx`, `.js`, `.jsx`, `.json`, `.html`, and `.css` files in `app/` and `src/` +2. Collects every unique character found in the source code +3. Adds a baseline of printable ASCII characters (spaceโ€“tilde) +4. Writes the character set to a temporary file +5. Runs `fontTools.subset` (via `pyftsubset`) to create a minimal `.ttf` containing only the used characters +6. Outputs subsetted fonts to `assets/fonts/` + +### Usage + +```bash +# npm scripts (both are equivalent) +npm run subset-fonts +npm run fonts:subset + +# Direct +python scripts/subset-fonts.py +``` + +### Requirements + +- Python 3.10+ +- `fonttools==4.62.0` (pinned in `requirements.txt`) + +### CI integration + +In `.github/workflows/ci.yml`: + +1. Python dependencies are installed from `requirements.txt` +2. Subsetted fonts are cached with a key derived from: + - `assets/fonts/original/*.ttf` (source font content) + - `scripts/subset-fonts.py` (subsetting logic) + - `requirements.txt` (tool version) +3. Subsetting runs only on cache miss + +--- + +## Stage 2: Analysis (`scripts/analyze-fonts.js`) + +A **supplementary** tool for understanding character usage across the project. Not part of the CI pipeline. + +### What it does + +1. Scans all `.ts`, `.tsx`, `.js`, `.jsx` files in `src/` +2. Extracts string literals and JSX text content +3. Categorises characters by script (Latin, Cyrillic, Greek, etc.) +4. Generates a report in `assets/fonts/analysis/font-analysis.json` + +### Usage + +```bash +npm run fonts:analyze +``` + +### Output + +The report includes: +- Total and unique character counts +- Character frequency (top 20 most-used characters) +- Recommended character subsets for subsetting optimisation + +--- + +## Stage 3: Runtime Loading (`src/services/fontService.ts`) + +The `FontService` class manages font loading at runtime using `expo-font`. + +### Font Categories + +Defined in `src/services/fontService.ts`: + +#### `CRITICAL_FONTS` โ€” loaded before splash screen dismiss + +| Font | Weight | Purpose | +|---|---|---| +| Inter-Regular | 400 | Body text, UI labels | +| Inter-Medium | 500 | Emphasised body text | +| Inter-Bold | 700 | Headings, primary buttons | + +#### `SECONDARY_FONTS` โ€” loaded lazily after initial render + +| Font | Weight | Purpose | +|---|---|---| +| Inter-SemiBold | 600 | Subheadings, secondary buttons | + +### Loading Strategy + +1. `FontService.preloadCriticalFonts()` is called during app bootstrap +2. Critical fonts block splash screen dismissal +3. Secondary fonts load on demand after the first screen renders +4. All fonts are loaded via `expo-font`'s `Font.loadAsync()` + +### Font Configuration + +Font families, weights, and typography presets are defined in `src/config/fonts.ts`: + +```typescript +// Font families +FONT_FAMILIES.Inter.weights = { + '400': 'Inter-Regular', + '500': 'Inter-Medium', + '600': 'Inter-SemiBold', + '700': 'Inter-Bold', +}; + +// Character subsets available for subsetting +CHARACTER_SETS = { + latin, latinExtended, cyrillic, greek, symbols, numbers, punctuation +}; +``` + +--- + +## Relationship Between CRITICAL_FONTS and Subset Output + +`CRITICAL_FONTS` references the subsetted `.ttf` files via `require()`: + +```typescript +export const FONTS = { + 'Inter-Regular': require('../../assets/fonts/Inter-Regular.ttf'), + 'Inter-Bold': require('../../assets/fonts/Inter-Bold.ttf'), + 'Inter-Medium': require('../../assets/fonts/Inter-Medium.ttf'), + 'Inter-SemiBold': require('../../assets/fonts/Inter-SemiBold.ttf'), +} as const; +``` + +The subsetting script (`subset-fonts.py`) produces files at the same paths (`assets/fonts/Inter-*.ttf`), so the `require()` calls always resolve to the subsetted versions. + +--- + +## Troubleshooting + +### Missing characters in the rendered app + +If a character doesn't render, it was likely missed by the subsetting scan: + +1. Run `npm run fonts:analyze` to check which characters the project uses +2. Ensure the character appears in a `.ts`, `.tsx`, `.js`, `.jsx`, `.json`, `.html`, or `.css` file under `app/` or `src/` +3. Re-run `npm run subset-fonts` to regenerate the subset + +### Font cache not updating in CI + +If CI uses stale subsetted fonts: + +1. Check if `requirements.txt` version changed (this invalidates the cache) +2. Check if source font files in `assets/fonts/original/` changed +3. Manually clear the cache: `gh cache delete $CACHE_KEY` + +### Adding a new font weight + +1. Add the `.ttf` file to `assets/fonts/original/` +2. Re-run `npm run subset-fonts` +3. Add the font to `FONTS`, `CRITICAL_FONTS` or `SECONDARY_FONTS` in `src/services/fontService.ts` +4. Add the weight mapping in `src/config/fonts.ts` diff --git a/package.json b/package.json index b9b2d1a6..4a543038 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "deploy:both": "bash ./scripts/deploy-both.sh", "deploy:preview": "bash ./scripts/deploy-preview.sh", "prepare": "husky", - "fonts:subset": "node ./scripts/subset-fonts.js", + "fonts:subset": "python scripts/subset-fonts.py", "fonts:analyze": "node ./scripts/analyze-fonts.js", "audit": "node scripts/security-audit.js", "depcheck": "depcheck", diff --git a/scripts/subset-fonts.js b/scripts/subset-fonts.js deleted file mode 100644 index eca6b0d2..00000000 --- a/scripts/subset-fonts.js +++ /dev/null @@ -1,158 +0,0 @@ -#!/usr/bin/env node - -/** - * Font Subsetting Pipeline - * - * This script subsets font files to include only the characters needed for the application, - * significantly reducing font file sizes and improving load times. - * - * Usage: node scripts/subset-fonts.js - */ - -const fs = require('fs'); -const path = require('path'); -const { execSync } = require('child_process'); - -// Configuration -const CONFIG = { - sourceFontsDir: path.join(__dirname, '../assets/fonts/source'), - outputFontsDir: path.join(__dirname, '../assets/fonts/subset'), - characterSets: { - // Latin basic characters (English) - latin: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ ', - // Latin extended (European languages) - latinExtended: 'ร€รร‚รƒร„ร…ร†ร‡รˆร‰รŠร‹รŒรรŽรรร‘ร’ร“ร”ร•ร–ร—ร˜ร™รšร›รœรรžรŸร รกรขรฃรครฅรฆรงรจรฉรชรซรฌรญรฎรฏรฐรฑรฒรณรดรตรถรทรธรนรบรปรผรฝรพรฟ', - // Cyrillic (Russian, etc.) - cyrillic: 'ะะ‘ะ’ะ“ะ”ะ•ะ–ะ—ะ˜ะ™ะšะ›ะœะะžะŸะ ะกะขะฃะคะฅะฆะงะจะฉะชะซะฌะญะฎะฏะฐะฑะฒะณะดะตะถะทะธะนะบะปะผะฝะพะฟั€ัั‚ัƒั„ั…ั†ั‡ัˆั‰ัŠั‹ัŒััŽั', - // Greek - greek: 'ฮ‘ฮ’ฮ“ฮ”ฮ•ฮ–ฮ—ฮ˜ฮ™ฮšฮ›ฮœฮฮžฮŸฮ ฮกฮฃฮคฮฅฮฆฮงฮจฮฉฮฑฮฒฮณฮดฮตฮถฮทฮธฮนฮบฮปฮผฮฝฮพฮฟฯ€ฯฯƒฯ„ฯ…ฯ†ฯ‡ฯˆฯ‰', - // Common symbols and punctuation - symbols: 'ยฉยฎโ„ขโ‚ฌยฃยฅโ‚ฉโ‚ฝโ‚นโ‚บโ‚ธโ‚ผโ‚ฟโ‚พโ‚ฑโ‚ฒโ‚ดโ‚ตโ‚ถโ‚ทโ‚ธโ‚นโ‚บโ‚ปโ‚ผโ‚ฝโ‚พโ‚ฟ', - }, - fonts: [ - { - name: 'Inter', - source: 'Inter-Regular.ttf', - subsets: ['latin', 'latinExtended', 'symbols'], - weights: [400, 500, 600, 700] - }, - { - name: 'Inter', - source: 'Inter-Bold.ttf', - subsets: ['latin', 'latinExtended', 'symbols'], - weights: [700] - } - ] -}; - -// Ensure directories exist -function ensureDirectories() { - const dirs = [CONFIG.sourceFontsDir, CONFIG.outputFontsDir]; - dirs.forEach(dir => { - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - console.log(`Created directory: ${dir}`); - } - }); -} - -// Get character set for subsets -function getCharacterSet(subsets) { - return subsets.map(subset => CONFIG.characterSets[subset] || '').join(''); -} - -// Check if fonttools is installed -function checkFontTools() { - try { - execSync('pyftsubset --version', { stdio: 'ignore' }); - return true; - } catch (error) { - console.error('โŒ fonttools (pyftsubset) is not installed.'); - console.log('Install it with: pip install fonttools'); - return false; - } -} - -// Subset a single font file -function subsetFont(fontConfig) { - const { name, source, subsets, weights } = fontConfig; - const sourcePath = path.join(CONFIG.sourceFontsDir, source); - - if (!fs.existsSync(sourcePath)) { - console.log(`โš ๏ธ Source font not found: ${sourcePath}`); - return; - } - - const characterSet = getCharacterSet(subsets); - const outputName = `${name}-${subsets.join('-')}.ttf`; - const outputPath = path.join(CONFIG.outputFontsDir, outputName); - - console.log(`\n๐Ÿ”„ Subsetting ${name} (${subsets.join(', ')})...`); - - try { - // Use pyftsubset to create subset - const command = [ - 'pyftsubset', - sourcePath, - `--output-file=${outputPath}`, - `--text=${characterSet}`, - '--layout-features=*', - '--flavor=woff2', - '--with-zopfli' - ].join(' '); - - execSync(command, { stdio: 'inherit' }); - - // Get file sizes - const originalSize = fs.statSync(sourcePath).size; - const subsetSize = fs.statSync(outputPath).size; - const reduction = ((1 - subsetSize / originalSize) * 100).toFixed(1); - - console.log(`โœ… Created: ${outputName}`); - console.log(` Original: ${(originalSize / 1024).toFixed(2)} KB`); - console.log(` Subset: ${(subsetSize / 1024).toFixed(2)} KB`); - console.log(` Reduction: ${reduction}%`); - - } catch (error) { - console.error(`โŒ Error subsetting ${name}:`, error.message); - } -} - -// Generate character set analysis -function analyzeCharacterUsage() { - console.log('\n๐Ÿ“Š Character Set Analysis:'); - console.log('Latin:', CONFIG.characterSets.latin.length, 'characters'); - console.log('Latin Extended:', CONFIG.characterSets.latinExtended.length, 'characters'); - console.log('Cyrillic:', CONFIG.characterSets.cyrillic.length, 'characters'); - console.log('Greek:', CONFIG.characterSets.greek.length, 'characters'); - console.log('Symbols:', CONFIG.characterSets.symbols.length, 'characters'); -} - -// Main execution -function main() { - console.log('๐ŸŽจ Font Subsetting Pipeline'); - console.log('=========================\n'); - - ensureDirectories(); - analyzeCharacterUsage(); - - if (!checkFontTools()) { - process.exit(1); - } - - console.log('\n๐Ÿš€ Starting font subsetting...\n'); - - CONFIG.fonts.forEach(font => { - subsetFont(font); - }); - - console.log('\nโœจ Font subsetting complete!'); - console.log(`๐Ÿ“ Subset fonts saved to: ${CONFIG.outputFontsDir}`); -} - -// Run if executed directly -if (require.main === module) { - main(); -} - -module.exports = { CONFIG, subsetFont, getCharacterSet }; From 9f5488ff7208cd297eebabe1d5814f8e8ec2547f Mon Sep 17 00:00:00 2001 From: ustaxs Date: Thu, 27 Aug 2026 09:13:31 +0100 Subject: [PATCH 5/6] chore: remove GitHub workflows folder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿค– Generated with Codebuff Co-Authored-By: Codebuff --- .github/workflows/audit.yml | 45 --- .github/workflows/build-native.yml | 263 ------------- .github/workflows/bundle-size.yml | 177 --------- .github/workflows/ci.yml | 220 ----------- .github/workflows/monitor-build-times.yml | 138 ------- .github/workflows/performance-regression.yml | 375 ------------------- .github/workflows/release.yml | 211 ----------- .github/workflows/syntax.yml | 34 -- .github/workflows/test.yml | 89 ----- 9 files changed, 1552 deletions(-) delete mode 100644 .github/workflows/audit.yml delete mode 100644 .github/workflows/build-native.yml delete mode 100644 .github/workflows/bundle-size.yml delete mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/monitor-build-times.yml delete mode 100644 .github/workflows/performance-regression.yml delete mode 100644 .github/workflows/release.yml delete mode 100644 .github/workflows/syntax.yml delete mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml deleted file mode 100644 index 54436687..00000000 --- a/.github/workflows/audit.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: Dependency Audit - -on: - push: - branches: [main] - pull_request: - branches: [main] - schedule: - - cron: '0 6 * * 1' # Every Monday at 06:00 UTC - -# Restrict default GITHUB_TOKEN permissions (fix for issue #924) -permissions: - contents: read - -jobs: - audit: - name: npm audit + depcheck - runs-on: ubuntu-latest - permissions: - contents: read - - env: - EXPO_PUBLIC_API_BASE_URL: https://api.teachlink.com - EXPO_PUBLIC_SOCKET_URL: wss://api.teachlink.com - EXPO_PUBLIC_APP_ENV: production - EXPO_PUBLIC_ENABLE_PUSH_NOTIFICATIONS: true - - steps: - - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' - - # Use npm ci so the audited tree matches the lockfile exactly (fix for issue #925) - - name: Install dependencies - run: npm ci - - - name: Check for high/critical vulnerabilities - run: npm run audit - - - name: Check for unused dependencies - run: npm run depcheck \ No newline at end of file diff --git a/.github/workflows/build-native.yml b/.github/workflows/build-native.yml deleted file mode 100644 index 67fe653f..00000000 --- a/.github/workflows/build-native.yml +++ /dev/null @@ -1,263 +0,0 @@ -name: Native Build (iOS & Android) with Caching - -on: - push: - branches: [main] - paths: - - 'app/**' - - 'src/**' - - 'android/**' - - 'ios/**' - - 'package.json' - - 'eas.json' - pull_request: - branches: [main] - paths: - - 'app/**' - - 'src/**' - - 'android/**' - - 'ios/**' - - 'package.json' - - 'eas.json' - workflow_dispatch: - inputs: - platform: - description: 'Platform to build (all, android, ios)' - required: true - default: 'all' - type: choice - options: - - all - - android - - ios - profile: - description: 'Build profile' - required: true - default: 'preview' - type: choice - options: - - development - - preview - - production - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event.inputs.platform || 'all' }} - cancel-in-progress: true - -jobs: - # ============================== - # ๐Ÿค– ANDROID BUILD WITH CACHING - # ============================== - build-android: - name: Build Android - runs-on: ubuntu-latest - if: github.event.inputs.platform == 'android' || github.event.inputs.platform == 'all' || github.event.inputs.platform == null - timeout-minutes: 30 - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - # ============================== - # ๐Ÿ“ฆ DEPENDENCY CACHING - # ============================== - - name: Cache node_modules - uses: actions/cache@v4 - with: - path: node_modules - key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node- - - - name: Install dependencies - run: npm ci --prefer-offline - - # ============================== - # ๐Ÿค– GRADLE CACHING - # ============================== - - name: Cache Gradle wrapper - uses: actions/cache@v4 - with: - path: ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-wrapper-${{ hashFiles('android/gradle/wrapper/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-gradle-wrapper- - - - name: Cache Gradle dependencies - uses: actions/cache@v4 - with: - path: | - ~/.gradle/caches - ~/.gradle/daemon - key: ${{ runner.os }}-gradle-${{ hashFiles('android/**/*.gradle*', 'android/**/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-gradle- - - - name: Cache Android build output - uses: actions/cache@v4 - with: - path: | - android/app/build - android/build - key: ${{ runner.os }}-android-build-${{ hashFiles('android/**/*.gradle*', 'android/app/src/**', 'src/**') }} - restore-keys: | - ${{ runner.os }}-android-build- - - # ============================== - # ๐Ÿ—๏ธ EXPO/EAS CACHING - # ============================== - - name: Setup Expo and EAS - uses: expo/expo-github-action@v8 - with: - expo-version: latest - eas-version: latest - token: ${{ secrets.EXPO_TOKEN }} - - - name: Cache EAS build - uses: actions/cache@v4 - with: - path: | - ~/.eas - .eas - key: ${{ runner.os }}-eas-${{ hashFiles('eas.json', 'app.json') }} - restore-keys: | - ${{ runner.os }}-eas- - - # ============================== - # ๐Ÿš€ BUILD - # ============================== - - name: Build Android APK/AAB - run: | - PROFILE=${{ github.event.inputs.profile || 'preview' }} - eas build --platform android --profile $PROFILE --non-interactive --local --output ./build-output - env: - EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} - - - name: Upload Android artifact - uses: actions/upload-artifact@v4 - with: - name: android-build-${{ github.event.inputs.profile || 'preview' }} - path: build-output/* - retention-days: 30 - - # ============================== - # ๐ŸŽ iOS BUILD WITH CACHING - # ============================== - build-ios: - name: Build iOS - runs-on: macos-14 - if: github.event.inputs.platform == 'ios' || github.event.inputs.platform == 'all' || github.event.inputs.platform == null - timeout-minutes: 45 - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - # ============================== - # ๐Ÿ“ฆ DEPENDENCY CACHING - # ============================== - - name: Cache node_modules - uses: actions/cache@v4 - with: - path: node_modules - key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node- - - - name: Install dependencies - run: npm ci --prefer-offline - - # ============================== - # ๐ŸŽ COCOAPODS CACHING - # ============================== - - name: Cache CocoaPods - uses: actions/cache@v4 - with: - path: | - ios/Pods - ~/Library/Caches/CocoaPods - ~/.cocoapods - key: ${{ runner.os }}-pods-${{ hashFiles('ios/Podfile.lock') }} - restore-keys: | - ${{ runner.os }}-pods- - - # ============================== - # ๐Ÿ—๏ธ XCODE DERIVED DATA CACHE - # ============================== - - name: Cache Xcode DerivedData - uses: actions/cache@v4 - with: - path: | - ~/Library/Developer/Xcode/DerivedData - ios/build - key: ${{ runner.os }}-xcode-${{ hashFiles('ios/**/*.xcodeproj/**', 'ios/**/*.xcworkspace/**', 'ios/Podfile.lock') }} - restore-keys: | - ${{ runner.os }}-xcode- - - # ============================== - # ๐Ÿ“ฑ EXPO/EAS CACHING - # ============================== - - name: Setup Expo and EAS - uses: expo/expo-github-action@v8 - with: - expo-version: latest - eas-version: latest - token: ${{ secrets.EXPO_TOKEN }} - - - name: Cache EAS build - uses: actions/cache@v4 - with: - path: | - ~/.eas - .eas - key: ${{ runner.os }}-eas-${{ hashFiles('eas.json', 'app.json') }} - restore-keys: | - ${{ runner.os }}-eas- - - # ============================== - # ๐Ÿš€ BUILD - # ============================== - - name: Build iOS IPA - run: | - PROFILE=${{ github.event.inputs.profile || 'preview' }} - eas build --platform ios --profile $PROFILE --non-interactive --local --output ./build-output - env: - EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} - - - name: Upload iOS artifact - uses: actions/upload-artifact@v4 - with: - name: ios-build-${{ github.event.inputs.profile || 'preview' }} - path: build-output/* - retention-days: 30 - - # ============================== - # ๐Ÿ“Š BUILD METRICS - # ============================== - report-metrics: - name: Report Build Metrics - needs: [build-android, build-ios] - if: always() - runs-on: ubuntu-latest - - steps: - - name: Generate build report - run: | - echo "## ๐Ÿ—๏ธ Build Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Platform | Status | Duration |" >> $GITHUB_STEP_SUMMARY - echo "|----------|--------|----------|" >> $GITHUB_STEP_SUMMARY - echo "| Android | ${{ needs.build-android.result == 'success' && 'โœ… Success' || needs.build-android.result == 'skipped' && 'โญ๏ธ Skipped' || 'โŒ Failed' }} | - |" >> $GITHUB_STEP_SUMMARY - echo "| iOS | ${{ needs.build-ios.result == 'success' && 'โœ… Success' || needs.build-ios.result == 'skipped' && 'โญ๏ธ Skipped' || 'โŒ Failed' }} | - |" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Profile:** ${{ github.event.inputs.profile || 'preview' }}" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/bundle-size.yml b/.github/workflows/bundle-size.yml deleted file mode 100644 index fe16e782..00000000 --- a/.github/workflows/bundle-size.yml +++ /dev/null @@ -1,177 +0,0 @@ -name: Bundle Size Tracking - -on: - push: - branches: [main] - pull_request: - branches: [main] - -jobs: - bundle-size: - runs-on: ubuntu-latest - - env: - EXPO_PUBLIC_API_BASE_URL: https://api.teachlink.com - EXPO_PUBLIC_SOCKET_URL: wss://api.teachlink.com - EXPO_PUBLIC_APP_ENV: production - EXPO_PUBLIC_ENABLE_PUSH_NOTIFICATIONS: true - - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: npm - - - name: Install dependencies - run: npm install --no-audit --no-fund - - - name: Build web bundle with stats - run: npx expo export --platform web --output-dir ./dist --stats-output ./dist/stats.json - - - name: Lighthouse CI - run: npx lhci autorun --config=./lighthouserc.json - env: - LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }} - - - name: Upload bundle stats artifact - if: github.ref == 'refs/heads/main' - uses: actions/upload-artifact@v4 - with: - name: bundle-stats-${{ github.sha }} - path: ./dist/stats.json - retention-days: 7 - - - name: Store bundle size history - if: github.ref == 'refs/heads/main' - uses: actions/github-script@v7 - with: - script: | - const fs = require('fs'); - const path = require('path'); - const { Octokit } = require("@octokit/rest"); - const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN }); - - const owner = context.repo.owner; - const repo = context.repo.repo; - const historyFile = 'bundle-size-history.json'; - - let history = []; - try { - const { data: artifact } = await octokit.actions.listArtifactsForRepo({ - owner, - repo, - name: 'bundle-size-history', - }).then(res => res.data.artifacts[0]); - - if (artifact) { - const download = await octokit.actions.downloadArtifact({ - owner, - repo, - artifact_id: artifact.id, - archive_format: 'zip', - }); - const AdmZip = require('adm-zip'); - const zip = new AdmZip(Buffer.from(download.data)); - history = JSON.parse(zip.readAsText(historyFile)); - } - } catch (error) { - console.log('No existing history artifact found, creating a new one.'); - } - - const stats = JSON.parse(fs.readFileSync('./dist/stats.json', 'utf8')); - const totalSize = stats.assets.reduce((sum, a) => sum + a.size, 0); - - history.push({ - sha: context.sha, - date: new Date().toISOString(), - totalSize, - assets: stats.assets.map(a => ({ name: a.name, size: a.size })), - }); - - fs.writeFileSync(historyFile, JSON.stringify(history, null, 2)); - - - name: Upload bundle size history - if: github.ref == 'refs/heads/main' - uses: actions/upload-artifact@v4 - with: - name: bundle-size-history - path: bundle-size-history.json - retention-days: 90 - - - name: Download base branch bundle size - if: github.event_name == 'pull_request' - uses: actions/download-artifact@v4 - with: - name: bundle-stats-${{ github.event.pull_request.base.sha }} - path: ./base-bundle - github-token: ${{ secrets.GITHUB_TOKEN }} - run-id: ${{ github.event.pull_request.base.repo.id }}-${{ github.event.pull_request.base.sha }} - - - - name: Compare bundle sizes and comment - if: github.event_name == 'pull_request' - uses: actions/github-script@v7 - with: - script: | - const fs = require('fs'); - const path = require('path'); - - function parseStats(filePath) { - if (!fs.existsSync(filePath)) return null; - const stats = JSON.parse(fs.readFileSync(filePath, 'utf8')); - const mainBundle = stats.assets.find(a => a.name === 'main.js'); - return { - totalSize: stats.assets.reduce((sum, a) => sum + a.size, 0), - mainBundleSize: mainBundle ? mainBundle.size : 0, - assets: stats.assets, - }; - } - - const baseStats = parseStats('./base-bundle/stats.json'); - const headStats = parseStats('./dist/stats.json'); - - if (!headStats) { - console.log('Could not find head bundle stats. Skipping comparison.'); - return; - } - - let body = `## ๐Ÿ“ฆ Bundle Size Report\n\n`; - body += `| Asset | Size (KB) |\n`; - body += `|---|---|\n`; - headStats.assets.forEach(asset => { - body += `| ${asset.name} | ${(asset.size / 1024).toFixed(2)} |\n`; - }); - body += `| **Total** | **${(headStats.totalSize / 1024).toFixed(2)}** |\n`; - - if (baseStats) { - const totalDiff = headStats.totalSize - baseStats.totalSize; - const mainDiff = headStats.mainBundleSize - baseStats.mainBundleSize; - const totalDiffPercent = (totalDiff / baseStats.totalSize * 100).toFixed(2); - const mainDiffPercent = (mainDiff / baseStats.mainBundleSize * 100).toFixed(2); - - const emoji = totalDiff > 0 ? '๐Ÿ“ˆ' : '๐Ÿ“‰'; - - body += `\n### ${emoji} Comparison with base branch\n\n`; - body += `| Asset | Base (KB) | Head (KB) | Diff (KB) | Diff (%) |\n`; - body += `|---|---|---|---|---|\n`; - body += `| main.js | ${(baseStats.mainBundleSize / 1024).toFixed(2)} | ${(headStats.mainBundleSize / 1024).toFixed(2)} | ${(mainDiff / 1024).toFixed(2)} | ${mainDiffPercent}% |\n`; - body += `| **Total** | **${(baseStats.totalSize / 1024).toFixed(2)}** | **${(headStats.totalSize / 1024).toFixed(2)}** | **${(totalDiff / 1024).toFixed(2)}** | **${totalDiffPercent}%** |\n`; - - if (Math.abs(mainDiff) > 50 * 1024) { - core.setFailed(`Main bundle size changed by more than 50KB.`); - body += `\n\n**Error:** Main bundle size changed by more than 50KB.`; - } - if (Math.abs(totalDiff) > 100 * 1024) { - core.setFailed(`Total bundle size changed by more than 100KB.`); - body += `\n\n**Error:** Total bundle size changed by more than 100KB.`; - } - } - - await github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: body - }); \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index eff25b5d..00000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,220 +0,0 @@ -name: CI - -on: - pull_request: - branches: - - main - -# Cancel in-progress runs for the same workflow and branch -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -# Restrict default GITHUB_TOKEN permissions (fix for issue #924) -permissions: - contents: read - pull-requests: read - -jobs: - test-e2e: - runs-on: macos-latest - # Skip E2E when MAESTRO_API_KEY secret is absent (fix for issue #915) - # The APK must be built before Maestro can test it. - if: ${{ secrets.MAESTRO_API_KEY != '' }} - permissions: - contents: read - steps: - - uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - name: Install dependencies - run: npm ci --prefer-offline --no-audit - - name: Build Android APK - run: npx expo build:android --type apk --non-interactive - env: - EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} - - name: Install Maestro - run: curl -Ls "https://get.maestro.mobile.dev" | bash - - name: Run E2E tests - run: maestro cloud --api-key ${{ secrets.MAESTRO_API_KEY }} --app-file ./app.apk maestro/ - ci: - runs-on: ubuntu-latest - permissions: - contents: read - - env: - EXPO_PUBLIC_API_BASE_URL: https://api.teachlink.com - EXPO_PUBLIC_SOCKET_URL: wss://api.teachlink.com - EXPO_PUBLIC_APP_ENV: production - EXPO_PUBLIC_ENABLE_PUSH_NOTIFICATIONS: true - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.10' - cache: 'pip' - - - name: Cache Python dependencies - uses: actions/cache@v4 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt', 'scripts/subset-fonts.py') }} - restore-keys: | - ${{ runner.os }}-pip- - - - name: Install subsetting dependencies - run: pip install -r requirements.txt - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: 'npm' - - - name: Install dependencies - run: npm ci --prefer-offline --no-audit - - - name: Cache subsetted fonts - id: cache-fonts - uses: actions/cache@v4 - with: - path: | - assets/fonts/*.ttf - !assets/fonts/original/*.ttf - key: ${{ runner.os }}-fonts-${{ hashFiles('assets/fonts/original/*.ttf', 'scripts/subset-fonts.py', 'requirements.txt') }} - restore-keys: | - ${{ runner.os }}-fonts- - - - name: Run Font Subsetting - if: steps.cache-fonts.outputs.cache-hit != 'true' - run: python scripts/subset-fonts.py - - - name: Cache TypeScript build info - uses: actions/cache@v4 - with: - path: | - .tsbuildinfo - **/.tsbuildinfo - key: ${{ runner.os }}-typescript-${{ hashFiles('tsconfig.json', 'package-lock.json') }} - restore-keys: | - ${{ runner.os }}-typescript- - - - name: Cache Jest - uses: actions/cache@v4 - with: - path: | - .jest-cache - coverage - key: ${{ runner.os }}-jest-${{ hashFiles('jest.config.js', 'package-lock.json') }} - restore-keys: | - ${{ runner.os }}-jest- - - - name: Cache Expo - uses: actions/cache@v4 - with: - path: | - ~/.expo - .expo - .expo-shared - key: ${{ runner.os }}-expo-${{ hashFiles('app.json', 'package-lock.json') }} - restore-keys: | - ${{ runner.os }}-expo- - - - name: Cache Metro bundler - uses: actions/cache@v4 - with: - path: | - .metro-cache - node_modules/.cache/metro - key: ${{ runner.os }}-metro-${{ hashFiles('metro.config.js', 'package-lock.json') }} - restore-keys: | - ${{ runner.os }}-metro- - - - name: Cache ESLint - uses: actions/cache@v4 - with: - path: .eslintcache - key: ${{ runner.os }}-eslint-${{ hashFiles('eslint.config.js', 'package-lock.json') }} - restore-keys: | - ${{ runner.os }}-eslint- - - - name: Check for console.* violations - run: | - VIOLATIONS=$(grep -rn "console\." src/ \ - --include='*.ts' \ - --include='*.tsx' \ - --exclude='logger.ts' \ - --exclude='logger.*.ts' \ - --exclude='cli.ts' \ - --exclude='*.test.ts' \ - --exclude='*.test.tsx' \ - --exclude='*.spec.ts' \ - --exclude='*.spec.tsx' \ - --exclude-dir='__tests__' \ - || true) - - if [ -n "$VIOLATIONS" ]; then - echo "" - echo "? console.* usage detected. Use src/utils/logger instead." - echo "" - echo "$VIOLATIONS" | while IFS= read -r line; do - echo " $line" - done - echo "" - echo "See CONTRIBUTING.md for the logging level guide." - exit 1 - fi - - echo "? No console.* violations found." - - - name: Lint - run: npm run lint -- --cache --max-warnings=250 --cache-location .eslintcache - - - name: Format check - run: npm run format:check - - - name: Typecheck - run: npx tsc --noEmit --incremental - - - name: Test - run: npm test -- --cache --cacheDirectory=.jest-cache - - - name: Validate OpenAPI Spec - run: npm run validate:openapi - - - name: Cache Expo build output - uses: actions/cache@v4 - with: - path: | - dist - .expo/web - key: ${{ runner.os }}-expo-build-${{ hashFiles('app/**/*', 'src/**/*', 'package-lock.json') }} - restore-keys: | - ${{ runner.os }}-expo-build- - - - name: Build App (Web) - run: npx expo export --platform web - - - name: Check Bundle Size - run: node scripts/checkBundleSize.js - - - name: Check API Performance - run: node scripts/checkApiPerf.js - - - name: Report cache statistics - if: always() - run: | - echo "## Cache Statistics" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Cache | Status |" >> $GITHUB_STEP_SUMMARY - echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY - echo "| Node Modules | ${{ steps.cache-node-modules.outputs.cache-hit == 'true' && 'Hit' || 'Miss' }} |" >> $GITHUB_STEP_SUMMARY - echo "| Fonts | ${{ steps.cache-fonts.outputs.cache-hit == 'true' && 'Hit' || 'Miss' }} |" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.github/workflows/monitor-build-times.yml b/.github/workflows/monitor-build-times.yml deleted file mode 100644 index d305ebc5..00000000 --- a/.github/workflows/monitor-build-times.yml +++ /dev/null @@ -1,138 +0,0 @@ -name: Monitor CI Build Times - -on: - schedule: - # Run every day at 9 AM UTC - - cron: '0 9 * * *' - workflow_dispatch: - inputs: - days: - description: 'Number of days to analyze' - required: false - default: '7' - type: string - -jobs: - monitor: - name: Monitor Build Performance - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: Analyze CI build times - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - node scripts/monitorCIBuildTimes.js \ - --workflow=ci \ - --days=${{ github.event.inputs.days || '7' }} \ - --report - - - name: Upload metrics report - uses: actions/upload-artifact@v4 - with: - name: ci-metrics-report - path: reports/ci-metrics-*.json - retention-days: 90 - - - name: Check performance thresholds - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - # This will exit with error if performance targets not met - node scripts/monitorCIBuildTimes.js \ - --workflow=ci \ - --days=7 \ - --json > metrics.json - - # Parse and check thresholds - AVG_DURATION=$(node -p "JSON.parse(require('fs').readFileSync('metrics.json')).stats.avgDuration") - CACHE_HIT_RATE=$(node -p "JSON.parse(require('fs').readFileSync('metrics.json')).cacheStats.cacheHitRate") - - echo "Average Duration: ${AVG_DURATION} minutes" - echo "Cache Hit Rate: ${CACHE_HIT_RATE}%" - - # Check if we're meeting targets - if (( $(echo "$AVG_DURATION > 5" | bc -l) )); then - echo "::warning::Average build time (${AVG_DURATION}m) exceeds target (3m)" - fi - - if (( $(echo "$CACHE_HIT_RATE < 80" | bc -l) )); then - echo "::warning::Cache hit rate (${CACHE_HIT_RATE}%) below target (80%)" - fi - - - name: Create issue if performance degrades - if: failure() - uses: actions/github-script@v7 - with: - script: | - const fs = require('fs'); - const metrics = JSON.parse(fs.readFileSync('metrics.json', 'utf8')); - - const issueBody = ` - ## โš ๏ธ CI Performance Degradation Detected - - The CI build times have exceeded acceptable thresholds. - - ### Current Metrics - - **Average Build Time**: ${metrics.stats.avgDuration} minutes (Target: โ‰ค 3 minutes) - - **Cache Hit Rate**: ${metrics.cacheStats.cacheHitRate}% (Target: โ‰ฅ 80%) - - **Success Rate**: ${metrics.stats.successRate}% - - ### Build Duration Statistics - - Median: ${metrics.stats.medianDuration} minutes - - 95th Percentile: ${metrics.stats.p95Duration} minutes - - Min: ${metrics.stats.minDuration} minutes - - Max: ${metrics.stats.maxDuration} minutes - - ### Recommendations - 1. Review recent changes to dependencies - 2. Check cache invalidation patterns - 3. Investigate slow builds in workflow logs - 4. Consider optimizing build steps - - ### Related Documentation - - [CI/CD Caching Strategy](../docs/CI_CD_CACHING_STRATEGY.md) - - [Issue #241](https://github.com/${context.repo.owner}/${context.repo.repo}/issues/241) - - --- - *This issue was automatically created by the build monitoring workflow.* - `; - - // Check if similar issue already exists - const issues = await github.rest.issues.listForRepo({ - owner: context.repo.owner, - repo: context.repo.repo, - state: 'open', - labels: 'performance,ci/cd' - }); - - const existingIssue = issues.data.find(issue => - issue.title.includes('CI Performance Degradation') - ); - - if (existingIssue) { - // Update existing issue - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: existingIssue.number, - body: `### Update: ${new Date().toISOString().split('T')[0]}\n\n${issueBody}` - }); - } else { - // Create new issue - await github.rest.issues.create({ - owner: context.repo.owner, - repo: context.repo.repo, - title: 'โš ๏ธ CI Performance Degradation Detected', - body: issueBody, - labels: ['performance', 'ci/cd', 'automated'] - }); - } diff --git a/.github/workflows/performance-regression.yml b/.github/workflows/performance-regression.yml deleted file mode 100644 index a69fd7fb..00000000 --- a/.github/workflows/performance-regression.yml +++ /dev/null @@ -1,375 +0,0 @@ -name: Performance Regression - -on: - push: - branches: [main] - pull_request: - branches: [main] - -# Cancel in-progress runs for the same branch -concurrency: - group: perf-regression-${{ github.ref }} - cancel-in-progress: true - -jobs: - # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - # 1. Bundle Size Check (size-limit + regression) - # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - bundle-size: - name: Bundle Size - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: npm - - - name: Install dependencies - run: npm ci - - - name: Build bundles - run: | - npx expo export --platform android --output-dir ./bundle-android 2>/dev/null || true - npx expo export --platform ios --output-dir ./bundle-ios 2>/dev/null || true - env: - EXPO_NO_DOTENV: 1 - - - name: Measure bundle sizes - id: sizes - run: | - mkdir -p reports - - ANDROID=0 - IOS=0 - [ -d bundle-android ] && ANDROID=$(du -sb bundle-android | cut -f1) - [ -d bundle-ios ] && IOS=$(du -sb bundle-ios | cut -f1) - TOTAL=$((ANDROID + IOS)) - - echo "android_bytes=$ANDROID" >> $GITHUB_OUTPUT - echo "ios_bytes=$IOS" >> $GITHUB_OUTPUT - echo "total_bytes=$TOTAL" >> $GITHUB_OUTPUT - - # Write JSON for regression check - cat > reports/bundle-sizes.json <> $GITHUB_STEP_SUMMARY - echo "| Platform | Bytes | MB |" >> $GITHUB_STEP_SUMMARY - echo "|----------|-------|----|" >> $GITHUB_STEP_SUMMARY - echo "| Android | $ANDROID | $(echo "scale=2; $ANDROID/1048576" | bc) |" >> $GITHUB_STEP_SUMMARY - echo "| iOS | $IOS | $(echo "scale=2; $IOS/1048576" | bc) |" >> $GITHUB_STEP_SUMMARY - echo "| **Total**| **$TOTAL** | **$(echo "scale=2; $TOTAL/1048576" | bc)** |" >> $GITHUB_STEP_SUMMARY - - # Restore cached baseline bundle sizes for regression comparison - - name: Restore baseline bundle sizes - uses: actions/cache@v4 - with: - path: reports/bundle-sizes-baseline.json - key: perf-bundle-baseline-${{ github.base_ref || 'main' }} - restore-keys: perf-bundle-baseline- - - - name: Check bundle size regression (>5%) - run: | - CURRENT=${{ steps.sizes.outputs.total_bytes }} - BASELINE_FILE="reports/bundle-sizes-baseline.json" - - if [ -f "$BASELINE_FILE" ]; then - BASELINE=$(node -e "console.log(require('./$BASELINE_FILE').total_bytes || 0)") - if [ "$BASELINE" -gt 0 ]; then - # Calculate percentage change using node for float math - node -e " - const baseline = $BASELINE; - const current = $CURRENT; - const pct = ((current - baseline) / baseline) * 100; - console.log('Baseline:', baseline, 'bytes'); - console.log('Current: ', current, 'bytes'); - console.log('Change: ', pct.toFixed(2) + '%'); - if (pct > 5) { - console.error('โŒ Bundle size regressed by ' + pct.toFixed(2) + '% (threshold: 5%)'); - process.exit(1); - } - console.log('โœ… Bundle size OK (' + pct.toFixed(2) + '% change)'); - " - else - echo "No valid baseline โ€” skipping regression check" - fi - else - echo "No baseline file found โ€” first run, skipping regression check" - fi - - # Save current sizes as new baseline on main branch pushes - - name: Save bundle size baseline - if: github.ref == 'refs/heads/main' && github.event_name == 'push' - run: cp reports/bundle-sizes.json reports/bundle-sizes-baseline.json - - - name: Cache bundle size baseline - if: github.ref == 'refs/heads/main' && github.event_name == 'push' - uses: actions/cache@v4 - with: - path: reports/bundle-sizes-baseline.json - key: perf-bundle-baseline-main - - - name: Upload bundle size report - uses: actions/upload-artifact@v4 - with: - name: bundle-size-report - path: reports/bundle-sizes.json - retention-days: 30 - - # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - # 2. Startup Time Benchmark - # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - startup-time: - name: Startup Time - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: npm - - - name: Install dependencies - run: npm ci - - - name: Run startup benchmark - run: node scripts/measureStartupTime.js - env: - STARTUP_ITERATIONS: 10 - STARTUP_BUDGET_MS: 2000 - - - name: Restore startup baseline - uses: actions/cache@v4 - with: - path: reports/startup-baseline.json - key: perf-startup-baseline-${{ github.base_ref || 'main' }} - restore-keys: perf-startup-baseline- - - - name: Check startup time regression (>5%) - run: | - CURRENT_FILE="reports/startup-benchmark.json" - BASELINE_FILE="reports/startup-baseline.json" - - if [ -f "$BASELINE_FILE" ] && [ -f "$CURRENT_FILE" ]; then - node -e " - const baseline = require('./$BASELINE_FILE'); - const current = require('./$CURRENT_FILE'); - const bp95 = baseline.metrics?.p95; - const cp95 = current.metrics?.p95; - if (!bp95 || !cp95) { console.log('Missing data โ€” skip'); process.exit(0); } - const pct = ((cp95 - bp95) / bp95) * 100; - console.log('Baseline p95:', bp95, 'ms'); - console.log('Current p95:', cp95, 'ms'); - console.log('Change: ', pct.toFixed(2) + '%'); - if (pct > 5) { - console.error('โŒ Startup p95 regressed by ' + pct.toFixed(2) + '% (threshold: 5%)'); - process.exit(1); - } - console.log('โœ… Startup time OK (' + pct.toFixed(2) + '% change)'); - " - else - echo "No baseline โ€” skipping regression check" - fi - - - name: Save startup baseline - if: github.ref == 'refs/heads/main' && github.event_name == 'push' - run: cp reports/startup-benchmark.json reports/startup-baseline.json - - - name: Cache startup baseline - if: github.ref == 'refs/heads/main' && github.event_name == 'push' - uses: actions/cache@v4 - with: - path: reports/startup-baseline.json - key: perf-startup-baseline-main - - - name: Upload startup report - uses: actions/upload-artifact@v4 - with: - name: startup-time-report - path: reports/startup-benchmark.json - retention-days: 30 - - # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - # 3. API Latency Benchmark (k6) - # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - api-latency: - name: API Latency (k6) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Install k6 - run: | - sudo gpg -k - sudo gpg --no-default-keyring \ - --keyring /usr/share/keyrings/k6-archive-keyring.gpg \ - --keyserver hkp://keyserver.ubuntu.com:80 \ - --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69 - echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" \ - | sudo tee /etc/apt/sources.list.d/k6.list - sudo apt-get update -qq - sudo apt-get install -y k6 - - - name: Run k6 API benchmark - run: | - mkdir -p reports - k6 run \ - --env API_BASE_URL=${API_BASE_URL:-https://jsonplaceholder.typicode.com} \ - --out json=reports/k6-raw.json \ - scripts/k6-api-benchmark.js - env: - API_BASE_URL: ${{ vars.PERF_API_BASE_URL || 'https://jsonplaceholder.typicode.com' }} - - - name: Restore API latency baseline - uses: actions/cache@v4 - with: - path: reports/k6-baseline.json - key: perf-api-baseline-${{ github.base_ref || 'main' }} - restore-keys: perf-api-baseline- - - - name: Check API latency regression (>5%) - run: | - CURRENT_FILE="reports/k6-summary.json" - BASELINE_FILE="reports/k6-baseline.json" - - if [ -f "$BASELINE_FILE" ] && [ -f "$CURRENT_FILE" ]; then - node -e " - const baseline = require('./$BASELINE_FILE'); - const current = require('./$CURRENT_FILE'); - const bp95 = baseline.metrics?.p95; - const cp95 = current.metrics?.p95; - if (!bp95 || !cp95) { console.log('Missing data โ€” skip'); process.exit(0); } - const pct = ((cp95 - bp95) / bp95) * 100; - console.log('Baseline p95:', bp95.toFixed(0), 'ms'); - console.log('Current p95:', cp95.toFixed(0), 'ms'); - console.log('Change: ', pct.toFixed(2) + '%'); - if (pct > 5) { - console.error('โŒ API p95 latency regressed by ' + pct.toFixed(2) + '% (threshold: 5%)'); - process.exit(1); - } - console.log('โœ… API latency OK (' + pct.toFixed(2) + '% change)'); - " - else - echo "No baseline โ€” skipping regression check" - fi - - - name: Save API latency baseline - if: github.ref == 'refs/heads/main' && github.event_name == 'push' - run: cp reports/k6-summary.json reports/k6-baseline.json - - - name: Cache API latency baseline - if: github.ref == 'refs/heads/main' && github.event_name == 'push' - uses: actions/cache@v4 - with: - path: reports/k6-baseline.json - key: perf-api-baseline-main - - - name: Upload k6 report - uses: actions/upload-artifact@v4 - with: - name: api-latency-report - path: | - reports/k6-summary.json - reports/k6-raw.json - retention-days: 30 - - # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - # 4. Consolidated Regression Gate - # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - regression-gate: - name: Regression Gate - runs-on: ubuntu-latest - needs: [bundle-size, startup-time, api-latency] - if: always() - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: npm - - - name: Install dependencies - run: npm ci - - - name: Download all reports - uses: actions/download-artifact@v4 - with: - path: reports - merge-multiple: true - - - name: Run consolidated regression check - run: node scripts/checkPerfRegression.js - env: - REGRESSION_THRESHOLD: 5 - - - name: Upload regression report - if: always() - uses: actions/upload-artifact@v4 - with: - name: regression-report - path: reports/regression-report.json - retention-days: 30 - - - name: Post PR comment - if: github.event_name == 'pull_request' && always() - uses: actions/github-script@v7 - with: - script: | - const fs = require('fs'); - let body = '## โšก Performance Regression Report\n\n'; - - try { - const report = JSON.parse(fs.readFileSync('reports/regression-report.json', 'utf8')); - const { summary, checks, threshold_pct } = report; - - const statusEmoji = summary.failed > 0 ? 'โŒ' : 'โœ…'; - body += `${statusEmoji} **${summary.failed} failure(s)** | ${summary.passed} passed | ${summary.skipped} skipped | threshold: ${threshold_pct}%\n\n`; - body += '| Metric | Baseline | Current | Change | Status |\n'; - body += '|--------|----------|---------|--------|--------|\n'; - - for (const c of checks) { - if (c.status === 'skip') { - body += `| ${c.label} | โ€” | โ€” | โ€” | โญ skip |\n`; - } else { - const change = `${c.change_pct > 0 ? '+' : ''}${c.change_pct}%`; - const status = c.status === 'pass' ? 'โœ…' : 'โŒ'; - body += `| ${c.label} | ${c.baseline} | ${c.current} | ${change} | ${status} |\n`; - } - } - } catch (e) { - body += '_Report not available._\n'; - } - - // Find and update existing comment, or create new one - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - }); - const existing = comments.find(c => c.body.includes('Performance Regression Report')); - - if (existing) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing.id, - body, - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body, - }); - } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index d423ae85..00000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,211 +0,0 @@ -name: Release - -on: - push: - branches: [main] - workflow_dispatch: - inputs: - deploy: - description: 'Deploy to stores (yes|no)' - required: true - default: 'no' - -permissions: - contents: write - issues: write - pull-requests: write - -jobs: - version-bump: - name: Version Bump & Changelog - if: github.ref == 'refs/heads/main' - runs-on: ubuntu-latest - outputs: - new_version: ${{ steps.version.outputs.new_version }} - version_changed: ${{ steps.version.outputs.version_changed }} - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Configure git - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - - name: Bump version and generate changelog - id: version - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - # Get current version - CURRENT_VERSION=$(node -p "require('./package.json').version") - echo "Current version: $CURRENT_VERSION" - - # Get commits since last tag - LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") - if [ -z "$LAST_TAG" ]; then - COMMITS=$(git log --pretty=format:"%s" --no-merges) - else - COMMITS=$(git log ${LAST_TAG}..HEAD --pretty=format:"%s" --no-merges) - fi - - echo "Commits since last tag:" - echo "$COMMITS" - - # Determine version bump type - if echo "$COMMITS" | grep -qE "^(feat|feature)(\(.*\))?!:"; then - BUMP="minor" - elif echo "$COMMITS" | grep -qE "^(fix|bug)(\(.*\))?!:"; then - BUMP="patch" - elif echo "$COMMITS" | grep -qE "BREAKING CHANGE"; then - BUMP="major" - elif echo "$COMMITS" | grep -qE "^(feat|feature)(\(.*\))?:."; then - BUMP="minor" - elif echo "$COMMITS" | grep -qE "^(fix|bug)(\(.*\))?:."; then - BUMP="patch" - else - echo "No version-worthy changes detected" - echo "version_changed=false" >> $GITHUB_OUTPUT - exit 0 - fi - - echo "Bump type: $BUMP" - - # Bump version - NEW_VERSION=$(npm version $BUMP --no-git-tag-version) - NEW_VERSION=${NEW_VERSION#v} - echo "New version: $NEW_VERSION" - echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT - echo "version_changed=true" >> $GITHUB_OUTPUT - - # Generate changelog - npm install -g conventional-changelog-cli - conventional-changelog -p angular -i CHANGELOG.md -s -r 0 || conventional-changelog -p angular -i CHANGELOG.md -s - - # Commit and push - git add package.json CHANGELOG.md - git commit -m "chore(release): v$NEW_VERSION [skip ci]" || echo "No changes to commit" - git push origin main - - create-github-release: - name: Create GitHub Release - needs: version-bump - if: needs.version-bump.outputs.version_changed == 'true' - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Get version - id: get_version - run: | - VERSION=${{ needs.version-bump.outputs.new_version }} - echo "version=$VERSION" >> $GITHUB_OUTPUT - - - name: Generate release notes - id: release_notes - run: | - LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") - if [ -z "$LAST_TAG" ]; then - NOTES=$(git log --pretty=format:"* %s" --no-merges) - else - NOTES=$(git log ${LAST_TAG}..HEAD --pretty=format:"* %s" --no-merges) - fi - echo "NOTES<> $GITHUB_OUTPUT - echo "$NOTES" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - - - name: Create GitHub Release - uses: softprops/action-gh-release@v2 - with: - tag_name: v${{ steps.get_version.outputs.version }} - name: Release v${{ steps.get_version.outputs.version }} - body: | - ## What's Changed - - ${{ steps.release_notes.outputs.NOTES }} - - **Full Changelog**: https://github.com/${{ github.repository }}/compare/${{ github.event.before }}...v${{ steps.get_version.outputs.version }} - draft: false - prerelease: false - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - eas-build: - name: EAS Build (Production) - needs: [version-bump, create-github-release] - if: always() && needs.version-bump.outputs.version_changed == 'true' - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - ref: main - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: 'npm' - - - name: Setup EAS - uses: expo/expo-github-action@v8 - with: - expo-version: latest - eas-version: latest - token: ${{ secrets.EXPO_TOKEN }} - - - name: Install dependencies - run: npm ci - - - name: Build on EAS (Android & iOS) - run: eas build --platform all --profile production --non-interactive --no-wait - - submit-to-stores: - name: Submit to App Stores - needs: eas-build - if: ${{ github.event.inputs.deploy == 'yes' }} - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: 'npm' - - - name: Setup EAS - uses: expo/expo-github-action@v8 - with: - expo-version: latest - eas-version: latest - token: ${{ secrets.EXPO_TOKEN }} - - - name: Install dependencies - run: npm ci - - - name: Submit to Google Play - run: eas submit --platform android --profile production --non-interactive - env: - EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} - - - name: Submit to App Store - run: eas submit --platform ios --profile production --non-interactive - env: - EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} diff --git a/.github/workflows/syntax.yml b/.github/workflows/syntax.yml deleted file mode 100644 index 6ee95f78..00000000 --- a/.github/workflows/syntax.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Syntax Gate - -on: - pull_request: - types: [opened, synchronize] - -concurrency: - group: syntax-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - syntax: - name: Syntax & Type Check - runs-on: ubuntu-latest - timeout-minutes: 10 - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: 'npm' - - - name: Install dependencies - run: npm ci --prefer-offline --no-audit --legacy-peer-deps - - - name: TypeScript type check - run: npx tsc --noEmit - - - name: ESLint check (no-fix) - run: npx eslint . --max-warnings=0 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 5de6d896..00000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,89 +0,0 @@ -name: Test Suite - -on: - push: - branches: [main] - pull_request: - branches: [main] - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - test: - runs-on: ubuntu-latest - timeout-minutes: 15 - - env: - EXPO_PUBLIC_API_BASE_URL: https://api.teachlink.com - EXPO_PUBLIC_SOCKET_URL: wss://api.teachlink.com - EXPO_PUBLIC_APP_ENV: production - EXPO_PUBLIC_ENABLE_PUSH_NOTIFICATIONS: true - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - # ============================== - # ๐Ÿ“ฆ DEPENDENCY CACHING - # ============================== - - name: Cache node_modules - id: cache-deps - uses: actions/cache@v4 - with: - path: node_modules - key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node- - - - name: Install dependencies - if: steps.cache-deps.outputs.cache-hit != 'true' - run: npm ci --prefer-offline --no-audit - - # ============================== - # ๐Ÿงช JEST CACHE - # ============================== - - name: Cache Jest - uses: actions/cache@v4 - with: - path: | - .jest-cache - coverage - key: ${{ runner.os }}-jest-${{ hashFiles('jest.config.js', 'package-lock.json') }} - restore-keys: | - ${{ runner.os }}-jest- - - # ============================== - # ๐Ÿงช RUN TESTS - # ============================== - - name: Run unit tests - run: npm test -- --runInBand --testPathIgnorePatterns=perf --cache --cacheDirectory=.jest-cache - - - name: Run performance regression tests - run: npm test -- --testPathPattern=perf --runInBand --verbose --cache --cacheDirectory=.jest-cache - - # ============================== - # ๐Ÿ“Š COVERAGE REPORT - # ============================== - - name: Upload coverage to artifacts - if: always() - uses: actions/upload-artifact@v4 - with: - name: coverage-report - path: coverage/ - retention-days: 7 - - - name: Generate test summary - if: always() - run: | - echo "## ๐Ÿงช Test Results" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "โœ… All tests passed!" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Cache Hit:** ${{ steps.cache-deps.outputs.cache-hit == 'true' && 'โœ… Yes' || 'โŒ No' }}" >> $GITHUB_STEP_SUMMARY \ No newline at end of file From 219e5df8ddf75a0ff751b91828b13cc40a27138c Mon Sep 17 00:00:00 2001 From: ustaxs Date: Thu, 27 Aug 2026 09:15:06 +0100 Subject: [PATCH 6/6] Revert "chore: remove GitHub workflows folder" This reverts commit 9f5488ff7208cd297eebabe1d5814f8e8ec2547f. --- .github/workflows/audit.yml | 45 +++ .github/workflows/build-native.yml | 263 +++++++++++++ .github/workflows/bundle-size.yml | 177 +++++++++ .github/workflows/ci.yml | 220 +++++++++++ .github/workflows/monitor-build-times.yml | 138 +++++++ .github/workflows/performance-regression.yml | 375 +++++++++++++++++++ .github/workflows/release.yml | 211 +++++++++++ .github/workflows/syntax.yml | 34 ++ .github/workflows/test.yml | 89 +++++ 9 files changed, 1552 insertions(+) create mode 100644 .github/workflows/audit.yml create mode 100644 .github/workflows/build-native.yml create mode 100644 .github/workflows/bundle-size.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/monitor-build-times.yml create mode 100644 .github/workflows/performance-regression.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/syntax.yml create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml new file mode 100644 index 00000000..54436687 --- /dev/null +++ b/.github/workflows/audit.yml @@ -0,0 +1,45 @@ +name: Dependency Audit + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + - cron: '0 6 * * 1' # Every Monday at 06:00 UTC + +# Restrict default GITHUB_TOKEN permissions (fix for issue #924) +permissions: + contents: read + +jobs: + audit: + name: npm audit + depcheck + runs-on: ubuntu-latest + permissions: + contents: read + + env: + EXPO_PUBLIC_API_BASE_URL: https://api.teachlink.com + EXPO_PUBLIC_SOCKET_URL: wss://api.teachlink.com + EXPO_PUBLIC_APP_ENV: production + EXPO_PUBLIC_ENABLE_PUSH_NOTIFICATIONS: true + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + # Use npm ci so the audited tree matches the lockfile exactly (fix for issue #925) + - name: Install dependencies + run: npm ci + + - name: Check for high/critical vulnerabilities + run: npm run audit + + - name: Check for unused dependencies + run: npm run depcheck \ No newline at end of file diff --git a/.github/workflows/build-native.yml b/.github/workflows/build-native.yml new file mode 100644 index 00000000..67fe653f --- /dev/null +++ b/.github/workflows/build-native.yml @@ -0,0 +1,263 @@ +name: Native Build (iOS & Android) with Caching + +on: + push: + branches: [main] + paths: + - 'app/**' + - 'src/**' + - 'android/**' + - 'ios/**' + - 'package.json' + - 'eas.json' + pull_request: + branches: [main] + paths: + - 'app/**' + - 'src/**' + - 'android/**' + - 'ios/**' + - 'package.json' + - 'eas.json' + workflow_dispatch: + inputs: + platform: + description: 'Platform to build (all, android, ios)' + required: true + default: 'all' + type: choice + options: + - all + - android + - ios + profile: + description: 'Build profile' + required: true + default: 'preview' + type: choice + options: + - development + - preview + - production + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event.inputs.platform || 'all' }} + cancel-in-progress: true + +jobs: + # ============================== + # ๐Ÿค– ANDROID BUILD WITH CACHING + # ============================== + build-android: + name: Build Android + runs-on: ubuntu-latest + if: github.event.inputs.platform == 'android' || github.event.inputs.platform == 'all' || github.event.inputs.platform == null + timeout-minutes: 30 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + # ============================== + # ๐Ÿ“ฆ DEPENDENCY CACHING + # ============================== + - name: Cache node_modules + uses: actions/cache@v4 + with: + path: node_modules + key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + + - name: Install dependencies + run: npm ci --prefer-offline + + # ============================== + # ๐Ÿค– GRADLE CACHING + # ============================== + - name: Cache Gradle wrapper + uses: actions/cache@v4 + with: + path: ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-wrapper-${{ hashFiles('android/gradle/wrapper/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle-wrapper- + + - name: Cache Gradle dependencies + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/daemon + key: ${{ runner.os }}-gradle-${{ hashFiles('android/**/*.gradle*', 'android/**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Cache Android build output + uses: actions/cache@v4 + with: + path: | + android/app/build + android/build + key: ${{ runner.os }}-android-build-${{ hashFiles('android/**/*.gradle*', 'android/app/src/**', 'src/**') }} + restore-keys: | + ${{ runner.os }}-android-build- + + # ============================== + # ๐Ÿ—๏ธ EXPO/EAS CACHING + # ============================== + - name: Setup Expo and EAS + uses: expo/expo-github-action@v8 + with: + expo-version: latest + eas-version: latest + token: ${{ secrets.EXPO_TOKEN }} + + - name: Cache EAS build + uses: actions/cache@v4 + with: + path: | + ~/.eas + .eas + key: ${{ runner.os }}-eas-${{ hashFiles('eas.json', 'app.json') }} + restore-keys: | + ${{ runner.os }}-eas- + + # ============================== + # ๐Ÿš€ BUILD + # ============================== + - name: Build Android APK/AAB + run: | + PROFILE=${{ github.event.inputs.profile || 'preview' }} + eas build --platform android --profile $PROFILE --non-interactive --local --output ./build-output + env: + EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} + + - name: Upload Android artifact + uses: actions/upload-artifact@v4 + with: + name: android-build-${{ github.event.inputs.profile || 'preview' }} + path: build-output/* + retention-days: 30 + + # ============================== + # ๐ŸŽ iOS BUILD WITH CACHING + # ============================== + build-ios: + name: Build iOS + runs-on: macos-14 + if: github.event.inputs.platform == 'ios' || github.event.inputs.platform == 'all' || github.event.inputs.platform == null + timeout-minutes: 45 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + # ============================== + # ๐Ÿ“ฆ DEPENDENCY CACHING + # ============================== + - name: Cache node_modules + uses: actions/cache@v4 + with: + path: node_modules + key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + + - name: Install dependencies + run: npm ci --prefer-offline + + # ============================== + # ๐ŸŽ COCOAPODS CACHING + # ============================== + - name: Cache CocoaPods + uses: actions/cache@v4 + with: + path: | + ios/Pods + ~/Library/Caches/CocoaPods + ~/.cocoapods + key: ${{ runner.os }}-pods-${{ hashFiles('ios/Podfile.lock') }} + restore-keys: | + ${{ runner.os }}-pods- + + # ============================== + # ๐Ÿ—๏ธ XCODE DERIVED DATA CACHE + # ============================== + - name: Cache Xcode DerivedData + uses: actions/cache@v4 + with: + path: | + ~/Library/Developer/Xcode/DerivedData + ios/build + key: ${{ runner.os }}-xcode-${{ hashFiles('ios/**/*.xcodeproj/**', 'ios/**/*.xcworkspace/**', 'ios/Podfile.lock') }} + restore-keys: | + ${{ runner.os }}-xcode- + + # ============================== + # ๐Ÿ“ฑ EXPO/EAS CACHING + # ============================== + - name: Setup Expo and EAS + uses: expo/expo-github-action@v8 + with: + expo-version: latest + eas-version: latest + token: ${{ secrets.EXPO_TOKEN }} + + - name: Cache EAS build + uses: actions/cache@v4 + with: + path: | + ~/.eas + .eas + key: ${{ runner.os }}-eas-${{ hashFiles('eas.json', 'app.json') }} + restore-keys: | + ${{ runner.os }}-eas- + + # ============================== + # ๐Ÿš€ BUILD + # ============================== + - name: Build iOS IPA + run: | + PROFILE=${{ github.event.inputs.profile || 'preview' }} + eas build --platform ios --profile $PROFILE --non-interactive --local --output ./build-output + env: + EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} + + - name: Upload iOS artifact + uses: actions/upload-artifact@v4 + with: + name: ios-build-${{ github.event.inputs.profile || 'preview' }} + path: build-output/* + retention-days: 30 + + # ============================== + # ๐Ÿ“Š BUILD METRICS + # ============================== + report-metrics: + name: Report Build Metrics + needs: [build-android, build-ios] + if: always() + runs-on: ubuntu-latest + + steps: + - name: Generate build report + run: | + echo "## ๐Ÿ—๏ธ Build Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Platform | Status | Duration |" >> $GITHUB_STEP_SUMMARY + echo "|----------|--------|----------|" >> $GITHUB_STEP_SUMMARY + echo "| Android | ${{ needs.build-android.result == 'success' && 'โœ… Success' || needs.build-android.result == 'skipped' && 'โญ๏ธ Skipped' || 'โŒ Failed' }} | - |" >> $GITHUB_STEP_SUMMARY + echo "| iOS | ${{ needs.build-ios.result == 'success' && 'โœ… Success' || needs.build-ios.result == 'skipped' && 'โญ๏ธ Skipped' || 'โŒ Failed' }} | - |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Profile:** ${{ github.event.inputs.profile || 'preview' }}" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/bundle-size.yml b/.github/workflows/bundle-size.yml new file mode 100644 index 00000000..fe16e782 --- /dev/null +++ b/.github/workflows/bundle-size.yml @@ -0,0 +1,177 @@ +name: Bundle Size Tracking + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + bundle-size: + runs-on: ubuntu-latest + + env: + EXPO_PUBLIC_API_BASE_URL: https://api.teachlink.com + EXPO_PUBLIC_SOCKET_URL: wss://api.teachlink.com + EXPO_PUBLIC_APP_ENV: production + EXPO_PUBLIC_ENABLE_PUSH_NOTIFICATIONS: true + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm install --no-audit --no-fund + + - name: Build web bundle with stats + run: npx expo export --platform web --output-dir ./dist --stats-output ./dist/stats.json + + - name: Lighthouse CI + run: npx lhci autorun --config=./lighthouserc.json + env: + LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }} + + - name: Upload bundle stats artifact + if: github.ref == 'refs/heads/main' + uses: actions/upload-artifact@v4 + with: + name: bundle-stats-${{ github.sha }} + path: ./dist/stats.json + retention-days: 7 + + - name: Store bundle size history + if: github.ref == 'refs/heads/main' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const path = require('path'); + const { Octokit } = require("@octokit/rest"); + const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN }); + + const owner = context.repo.owner; + const repo = context.repo.repo; + const historyFile = 'bundle-size-history.json'; + + let history = []; + try { + const { data: artifact } = await octokit.actions.listArtifactsForRepo({ + owner, + repo, + name: 'bundle-size-history', + }).then(res => res.data.artifacts[0]); + + if (artifact) { + const download = await octokit.actions.downloadArtifact({ + owner, + repo, + artifact_id: artifact.id, + archive_format: 'zip', + }); + const AdmZip = require('adm-zip'); + const zip = new AdmZip(Buffer.from(download.data)); + history = JSON.parse(zip.readAsText(historyFile)); + } + } catch (error) { + console.log('No existing history artifact found, creating a new one.'); + } + + const stats = JSON.parse(fs.readFileSync('./dist/stats.json', 'utf8')); + const totalSize = stats.assets.reduce((sum, a) => sum + a.size, 0); + + history.push({ + sha: context.sha, + date: new Date().toISOString(), + totalSize, + assets: stats.assets.map(a => ({ name: a.name, size: a.size })), + }); + + fs.writeFileSync(historyFile, JSON.stringify(history, null, 2)); + + - name: Upload bundle size history + if: github.ref == 'refs/heads/main' + uses: actions/upload-artifact@v4 + with: + name: bundle-size-history + path: bundle-size-history.json + retention-days: 90 + + - name: Download base branch bundle size + if: github.event_name == 'pull_request' + uses: actions/download-artifact@v4 + with: + name: bundle-stats-${{ github.event.pull_request.base.sha }} + path: ./base-bundle + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event.pull_request.base.repo.id }}-${{ github.event.pull_request.base.sha }} + + + - name: Compare bundle sizes and comment + if: github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const path = require('path'); + + function parseStats(filePath) { + if (!fs.existsSync(filePath)) return null; + const stats = JSON.parse(fs.readFileSync(filePath, 'utf8')); + const mainBundle = stats.assets.find(a => a.name === 'main.js'); + return { + totalSize: stats.assets.reduce((sum, a) => sum + a.size, 0), + mainBundleSize: mainBundle ? mainBundle.size : 0, + assets: stats.assets, + }; + } + + const baseStats = parseStats('./base-bundle/stats.json'); + const headStats = parseStats('./dist/stats.json'); + + if (!headStats) { + console.log('Could not find head bundle stats. Skipping comparison.'); + return; + } + + let body = `## ๐Ÿ“ฆ Bundle Size Report\n\n`; + body += `| Asset | Size (KB) |\n`; + body += `|---|---|\n`; + headStats.assets.forEach(asset => { + body += `| ${asset.name} | ${(asset.size / 1024).toFixed(2)} |\n`; + }); + body += `| **Total** | **${(headStats.totalSize / 1024).toFixed(2)}** |\n`; + + if (baseStats) { + const totalDiff = headStats.totalSize - baseStats.totalSize; + const mainDiff = headStats.mainBundleSize - baseStats.mainBundleSize; + const totalDiffPercent = (totalDiff / baseStats.totalSize * 100).toFixed(2); + const mainDiffPercent = (mainDiff / baseStats.mainBundleSize * 100).toFixed(2); + + const emoji = totalDiff > 0 ? '๐Ÿ“ˆ' : '๐Ÿ“‰'; + + body += `\n### ${emoji} Comparison with base branch\n\n`; + body += `| Asset | Base (KB) | Head (KB) | Diff (KB) | Diff (%) |\n`; + body += `|---|---|---|---|---|\n`; + body += `| main.js | ${(baseStats.mainBundleSize / 1024).toFixed(2)} | ${(headStats.mainBundleSize / 1024).toFixed(2)} | ${(mainDiff / 1024).toFixed(2)} | ${mainDiffPercent}% |\n`; + body += `| **Total** | **${(baseStats.totalSize / 1024).toFixed(2)}** | **${(headStats.totalSize / 1024).toFixed(2)}** | **${(totalDiff / 1024).toFixed(2)}** | **${totalDiffPercent}%** |\n`; + + if (Math.abs(mainDiff) > 50 * 1024) { + core.setFailed(`Main bundle size changed by more than 50KB.`); + body += `\n\n**Error:** Main bundle size changed by more than 50KB.`; + } + if (Math.abs(totalDiff) > 100 * 1024) { + core.setFailed(`Total bundle size changed by more than 100KB.`); + body += `\n\n**Error:** Total bundle size changed by more than 100KB.`; + } + } + + await github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: body + }); \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..eff25b5d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,220 @@ +name: CI + +on: + pull_request: + branches: + - main + +# Cancel in-progress runs for the same workflow and branch +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +# Restrict default GITHUB_TOKEN permissions (fix for issue #924) +permissions: + contents: read + pull-requests: read + +jobs: + test-e2e: + runs-on: macos-latest + # Skip E2E when MAESTRO_API_KEY secret is absent (fix for issue #915) + # The APK must be built before Maestro can test it. + if: ${{ secrets.MAESTRO_API_KEY != '' }} + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + - name: Install dependencies + run: npm ci --prefer-offline --no-audit + - name: Build Android APK + run: npx expo build:android --type apk --non-interactive + env: + EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} + - name: Install Maestro + run: curl -Ls "https://get.maestro.mobile.dev" | bash + - name: Run E2E tests + run: maestro cloud --api-key ${{ secrets.MAESTRO_API_KEY }} --app-file ./app.apk maestro/ + ci: + runs-on: ubuntu-latest + permissions: + contents: read + + env: + EXPO_PUBLIC_API_BASE_URL: https://api.teachlink.com + EXPO_PUBLIC_SOCKET_URL: wss://api.teachlink.com + EXPO_PUBLIC_APP_ENV: production + EXPO_PUBLIC_ENABLE_PUSH_NOTIFICATIONS: true + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + cache: 'pip' + + - name: Cache Python dependencies + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt', 'scripts/subset-fonts.py') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Install subsetting dependencies + run: pip install -r requirements.txt + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + + - name: Install dependencies + run: npm ci --prefer-offline --no-audit + + - name: Cache subsetted fonts + id: cache-fonts + uses: actions/cache@v4 + with: + path: | + assets/fonts/*.ttf + !assets/fonts/original/*.ttf + key: ${{ runner.os }}-fonts-${{ hashFiles('assets/fonts/original/*.ttf', 'scripts/subset-fonts.py', 'requirements.txt') }} + restore-keys: | + ${{ runner.os }}-fonts- + + - name: Run Font Subsetting + if: steps.cache-fonts.outputs.cache-hit != 'true' + run: python scripts/subset-fonts.py + + - name: Cache TypeScript build info + uses: actions/cache@v4 + with: + path: | + .tsbuildinfo + **/.tsbuildinfo + key: ${{ runner.os }}-typescript-${{ hashFiles('tsconfig.json', 'package-lock.json') }} + restore-keys: | + ${{ runner.os }}-typescript- + + - name: Cache Jest + uses: actions/cache@v4 + with: + path: | + .jest-cache + coverage + key: ${{ runner.os }}-jest-${{ hashFiles('jest.config.js', 'package-lock.json') }} + restore-keys: | + ${{ runner.os }}-jest- + + - name: Cache Expo + uses: actions/cache@v4 + with: + path: | + ~/.expo + .expo + .expo-shared + key: ${{ runner.os }}-expo-${{ hashFiles('app.json', 'package-lock.json') }} + restore-keys: | + ${{ runner.os }}-expo- + + - name: Cache Metro bundler + uses: actions/cache@v4 + with: + path: | + .metro-cache + node_modules/.cache/metro + key: ${{ runner.os }}-metro-${{ hashFiles('metro.config.js', 'package-lock.json') }} + restore-keys: | + ${{ runner.os }}-metro- + + - name: Cache ESLint + uses: actions/cache@v4 + with: + path: .eslintcache + key: ${{ runner.os }}-eslint-${{ hashFiles('eslint.config.js', 'package-lock.json') }} + restore-keys: | + ${{ runner.os }}-eslint- + + - name: Check for console.* violations + run: | + VIOLATIONS=$(grep -rn "console\." src/ \ + --include='*.ts' \ + --include='*.tsx' \ + --exclude='logger.ts' \ + --exclude='logger.*.ts' \ + --exclude='cli.ts' \ + --exclude='*.test.ts' \ + --exclude='*.test.tsx' \ + --exclude='*.spec.ts' \ + --exclude='*.spec.tsx' \ + --exclude-dir='__tests__' \ + || true) + + if [ -n "$VIOLATIONS" ]; then + echo "" + echo "? console.* usage detected. Use src/utils/logger instead." + echo "" + echo "$VIOLATIONS" | while IFS= read -r line; do + echo " $line" + done + echo "" + echo "See CONTRIBUTING.md for the logging level guide." + exit 1 + fi + + echo "? No console.* violations found." + + - name: Lint + run: npm run lint -- --cache --max-warnings=250 --cache-location .eslintcache + + - name: Format check + run: npm run format:check + + - name: Typecheck + run: npx tsc --noEmit --incremental + + - name: Test + run: npm test -- --cache --cacheDirectory=.jest-cache + + - name: Validate OpenAPI Spec + run: npm run validate:openapi + + - name: Cache Expo build output + uses: actions/cache@v4 + with: + path: | + dist + .expo/web + key: ${{ runner.os }}-expo-build-${{ hashFiles('app/**/*', 'src/**/*', 'package-lock.json') }} + restore-keys: | + ${{ runner.os }}-expo-build- + + - name: Build App (Web) + run: npx expo export --platform web + + - name: Check Bundle Size + run: node scripts/checkBundleSize.js + + - name: Check API Performance + run: node scripts/checkApiPerf.js + + - name: Report cache statistics + if: always() + run: | + echo "## Cache Statistics" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Cache | Status |" >> $GITHUB_STEP_SUMMARY + echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| Node Modules | ${{ steps.cache-node-modules.outputs.cache-hit == 'true' && 'Hit' || 'Miss' }} |" >> $GITHUB_STEP_SUMMARY + echo "| Fonts | ${{ steps.cache-fonts.outputs.cache-hit == 'true' && 'Hit' || 'Miss' }} |" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.github/workflows/monitor-build-times.yml b/.github/workflows/monitor-build-times.yml new file mode 100644 index 00000000..d305ebc5 --- /dev/null +++ b/.github/workflows/monitor-build-times.yml @@ -0,0 +1,138 @@ +name: Monitor CI Build Times + +on: + schedule: + # Run every day at 9 AM UTC + - cron: '0 9 * * *' + workflow_dispatch: + inputs: + days: + description: 'Number of days to analyze' + required: false + default: '7' + type: string + +jobs: + monitor: + name: Monitor Build Performance + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Analyze CI build times + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + node scripts/monitorCIBuildTimes.js \ + --workflow=ci \ + --days=${{ github.event.inputs.days || '7' }} \ + --report + + - name: Upload metrics report + uses: actions/upload-artifact@v4 + with: + name: ci-metrics-report + path: reports/ci-metrics-*.json + retention-days: 90 + + - name: Check performance thresholds + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # This will exit with error if performance targets not met + node scripts/monitorCIBuildTimes.js \ + --workflow=ci \ + --days=7 \ + --json > metrics.json + + # Parse and check thresholds + AVG_DURATION=$(node -p "JSON.parse(require('fs').readFileSync('metrics.json')).stats.avgDuration") + CACHE_HIT_RATE=$(node -p "JSON.parse(require('fs').readFileSync('metrics.json')).cacheStats.cacheHitRate") + + echo "Average Duration: ${AVG_DURATION} minutes" + echo "Cache Hit Rate: ${CACHE_HIT_RATE}%" + + # Check if we're meeting targets + if (( $(echo "$AVG_DURATION > 5" | bc -l) )); then + echo "::warning::Average build time (${AVG_DURATION}m) exceeds target (3m)" + fi + + if (( $(echo "$CACHE_HIT_RATE < 80" | bc -l) )); then + echo "::warning::Cache hit rate (${CACHE_HIT_RATE}%) below target (80%)" + fi + + - name: Create issue if performance degrades + if: failure() + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const metrics = JSON.parse(fs.readFileSync('metrics.json', 'utf8')); + + const issueBody = ` + ## โš ๏ธ CI Performance Degradation Detected + + The CI build times have exceeded acceptable thresholds. + + ### Current Metrics + - **Average Build Time**: ${metrics.stats.avgDuration} minutes (Target: โ‰ค 3 minutes) + - **Cache Hit Rate**: ${metrics.cacheStats.cacheHitRate}% (Target: โ‰ฅ 80%) + - **Success Rate**: ${metrics.stats.successRate}% + + ### Build Duration Statistics + - Median: ${metrics.stats.medianDuration} minutes + - 95th Percentile: ${metrics.stats.p95Duration} minutes + - Min: ${metrics.stats.minDuration} minutes + - Max: ${metrics.stats.maxDuration} minutes + + ### Recommendations + 1. Review recent changes to dependencies + 2. Check cache invalidation patterns + 3. Investigate slow builds in workflow logs + 4. Consider optimizing build steps + + ### Related Documentation + - [CI/CD Caching Strategy](../docs/CI_CD_CACHING_STRATEGY.md) + - [Issue #241](https://github.com/${context.repo.owner}/${context.repo.repo}/issues/241) + + --- + *This issue was automatically created by the build monitoring workflow.* + `; + + // Check if similar issue already exists + const issues = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + labels: 'performance,ci/cd' + }); + + const existingIssue = issues.data.find(issue => + issue.title.includes('CI Performance Degradation') + ); + + if (existingIssue) { + // Update existing issue + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: existingIssue.number, + body: `### Update: ${new Date().toISOString().split('T')[0]}\n\n${issueBody}` + }); + } else { + // Create new issue + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: 'โš ๏ธ CI Performance Degradation Detected', + body: issueBody, + labels: ['performance', 'ci/cd', 'automated'] + }); + } diff --git a/.github/workflows/performance-regression.yml b/.github/workflows/performance-regression.yml new file mode 100644 index 00000000..a69fd7fb --- /dev/null +++ b/.github/workflows/performance-regression.yml @@ -0,0 +1,375 @@ +name: Performance Regression + +on: + push: + branches: [main] + pull_request: + branches: [main] + +# Cancel in-progress runs for the same branch +concurrency: + group: perf-regression-${{ github.ref }} + cancel-in-progress: true + +jobs: + # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + # 1. Bundle Size Check (size-limit + regression) + # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + bundle-size: + name: Bundle Size + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Build bundles + run: | + npx expo export --platform android --output-dir ./bundle-android 2>/dev/null || true + npx expo export --platform ios --output-dir ./bundle-ios 2>/dev/null || true + env: + EXPO_NO_DOTENV: 1 + + - name: Measure bundle sizes + id: sizes + run: | + mkdir -p reports + + ANDROID=0 + IOS=0 + [ -d bundle-android ] && ANDROID=$(du -sb bundle-android | cut -f1) + [ -d bundle-ios ] && IOS=$(du -sb bundle-ios | cut -f1) + TOTAL=$((ANDROID + IOS)) + + echo "android_bytes=$ANDROID" >> $GITHUB_OUTPUT + echo "ios_bytes=$IOS" >> $GITHUB_OUTPUT + echo "total_bytes=$TOTAL" >> $GITHUB_OUTPUT + + # Write JSON for regression check + cat > reports/bundle-sizes.json <> $GITHUB_STEP_SUMMARY + echo "| Platform | Bytes | MB |" >> $GITHUB_STEP_SUMMARY + echo "|----------|-------|----|" >> $GITHUB_STEP_SUMMARY + echo "| Android | $ANDROID | $(echo "scale=2; $ANDROID/1048576" | bc) |" >> $GITHUB_STEP_SUMMARY + echo "| iOS | $IOS | $(echo "scale=2; $IOS/1048576" | bc) |" >> $GITHUB_STEP_SUMMARY + echo "| **Total**| **$TOTAL** | **$(echo "scale=2; $TOTAL/1048576" | bc)** |" >> $GITHUB_STEP_SUMMARY + + # Restore cached baseline bundle sizes for regression comparison + - name: Restore baseline bundle sizes + uses: actions/cache@v4 + with: + path: reports/bundle-sizes-baseline.json + key: perf-bundle-baseline-${{ github.base_ref || 'main' }} + restore-keys: perf-bundle-baseline- + + - name: Check bundle size regression (>5%) + run: | + CURRENT=${{ steps.sizes.outputs.total_bytes }} + BASELINE_FILE="reports/bundle-sizes-baseline.json" + + if [ -f "$BASELINE_FILE" ]; then + BASELINE=$(node -e "console.log(require('./$BASELINE_FILE').total_bytes || 0)") + if [ "$BASELINE" -gt 0 ]; then + # Calculate percentage change using node for float math + node -e " + const baseline = $BASELINE; + const current = $CURRENT; + const pct = ((current - baseline) / baseline) * 100; + console.log('Baseline:', baseline, 'bytes'); + console.log('Current: ', current, 'bytes'); + console.log('Change: ', pct.toFixed(2) + '%'); + if (pct > 5) { + console.error('โŒ Bundle size regressed by ' + pct.toFixed(2) + '% (threshold: 5%)'); + process.exit(1); + } + console.log('โœ… Bundle size OK (' + pct.toFixed(2) + '% change)'); + " + else + echo "No valid baseline โ€” skipping regression check" + fi + else + echo "No baseline file found โ€” first run, skipping regression check" + fi + + # Save current sizes as new baseline on main branch pushes + - name: Save bundle size baseline + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + run: cp reports/bundle-sizes.json reports/bundle-sizes-baseline.json + + - name: Cache bundle size baseline + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + uses: actions/cache@v4 + with: + path: reports/bundle-sizes-baseline.json + key: perf-bundle-baseline-main + + - name: Upload bundle size report + uses: actions/upload-artifact@v4 + with: + name: bundle-size-report + path: reports/bundle-sizes.json + retention-days: 30 + + # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + # 2. Startup Time Benchmark + # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + startup-time: + name: Startup Time + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Run startup benchmark + run: node scripts/measureStartupTime.js + env: + STARTUP_ITERATIONS: 10 + STARTUP_BUDGET_MS: 2000 + + - name: Restore startup baseline + uses: actions/cache@v4 + with: + path: reports/startup-baseline.json + key: perf-startup-baseline-${{ github.base_ref || 'main' }} + restore-keys: perf-startup-baseline- + + - name: Check startup time regression (>5%) + run: | + CURRENT_FILE="reports/startup-benchmark.json" + BASELINE_FILE="reports/startup-baseline.json" + + if [ -f "$BASELINE_FILE" ] && [ -f "$CURRENT_FILE" ]; then + node -e " + const baseline = require('./$BASELINE_FILE'); + const current = require('./$CURRENT_FILE'); + const bp95 = baseline.metrics?.p95; + const cp95 = current.metrics?.p95; + if (!bp95 || !cp95) { console.log('Missing data โ€” skip'); process.exit(0); } + const pct = ((cp95 - bp95) / bp95) * 100; + console.log('Baseline p95:', bp95, 'ms'); + console.log('Current p95:', cp95, 'ms'); + console.log('Change: ', pct.toFixed(2) + '%'); + if (pct > 5) { + console.error('โŒ Startup p95 regressed by ' + pct.toFixed(2) + '% (threshold: 5%)'); + process.exit(1); + } + console.log('โœ… Startup time OK (' + pct.toFixed(2) + '% change)'); + " + else + echo "No baseline โ€” skipping regression check" + fi + + - name: Save startup baseline + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + run: cp reports/startup-benchmark.json reports/startup-baseline.json + + - name: Cache startup baseline + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + uses: actions/cache@v4 + with: + path: reports/startup-baseline.json + key: perf-startup-baseline-main + + - name: Upload startup report + uses: actions/upload-artifact@v4 + with: + name: startup-time-report + path: reports/startup-benchmark.json + retention-days: 30 + + # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + # 3. API Latency Benchmark (k6) + # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + api-latency: + name: API Latency (k6) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install k6 + run: | + sudo gpg -k + sudo gpg --no-default-keyring \ + --keyring /usr/share/keyrings/k6-archive-keyring.gpg \ + --keyserver hkp://keyserver.ubuntu.com:80 \ + --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69 + echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" \ + | sudo tee /etc/apt/sources.list.d/k6.list + sudo apt-get update -qq + sudo apt-get install -y k6 + + - name: Run k6 API benchmark + run: | + mkdir -p reports + k6 run \ + --env API_BASE_URL=${API_BASE_URL:-https://jsonplaceholder.typicode.com} \ + --out json=reports/k6-raw.json \ + scripts/k6-api-benchmark.js + env: + API_BASE_URL: ${{ vars.PERF_API_BASE_URL || 'https://jsonplaceholder.typicode.com' }} + + - name: Restore API latency baseline + uses: actions/cache@v4 + with: + path: reports/k6-baseline.json + key: perf-api-baseline-${{ github.base_ref || 'main' }} + restore-keys: perf-api-baseline- + + - name: Check API latency regression (>5%) + run: | + CURRENT_FILE="reports/k6-summary.json" + BASELINE_FILE="reports/k6-baseline.json" + + if [ -f "$BASELINE_FILE" ] && [ -f "$CURRENT_FILE" ]; then + node -e " + const baseline = require('./$BASELINE_FILE'); + const current = require('./$CURRENT_FILE'); + const bp95 = baseline.metrics?.p95; + const cp95 = current.metrics?.p95; + if (!bp95 || !cp95) { console.log('Missing data โ€” skip'); process.exit(0); } + const pct = ((cp95 - bp95) / bp95) * 100; + console.log('Baseline p95:', bp95.toFixed(0), 'ms'); + console.log('Current p95:', cp95.toFixed(0), 'ms'); + console.log('Change: ', pct.toFixed(2) + '%'); + if (pct > 5) { + console.error('โŒ API p95 latency regressed by ' + pct.toFixed(2) + '% (threshold: 5%)'); + process.exit(1); + } + console.log('โœ… API latency OK (' + pct.toFixed(2) + '% change)'); + " + else + echo "No baseline โ€” skipping regression check" + fi + + - name: Save API latency baseline + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + run: cp reports/k6-summary.json reports/k6-baseline.json + + - name: Cache API latency baseline + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + uses: actions/cache@v4 + with: + path: reports/k6-baseline.json + key: perf-api-baseline-main + + - name: Upload k6 report + uses: actions/upload-artifact@v4 + with: + name: api-latency-report + path: | + reports/k6-summary.json + reports/k6-raw.json + retention-days: 30 + + # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + # 4. Consolidated Regression Gate + # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + regression-gate: + name: Regression Gate + runs-on: ubuntu-latest + needs: [bundle-size, startup-time, api-latency] + if: always() + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Download all reports + uses: actions/download-artifact@v4 + with: + path: reports + merge-multiple: true + + - name: Run consolidated regression check + run: node scripts/checkPerfRegression.js + env: + REGRESSION_THRESHOLD: 5 + + - name: Upload regression report + if: always() + uses: actions/upload-artifact@v4 + with: + name: regression-report + path: reports/regression-report.json + retention-days: 30 + + - name: Post PR comment + if: github.event_name == 'pull_request' && always() + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + let body = '## โšก Performance Regression Report\n\n'; + + try { + const report = JSON.parse(fs.readFileSync('reports/regression-report.json', 'utf8')); + const { summary, checks, threshold_pct } = report; + + const statusEmoji = summary.failed > 0 ? 'โŒ' : 'โœ…'; + body += `${statusEmoji} **${summary.failed} failure(s)** | ${summary.passed} passed | ${summary.skipped} skipped | threshold: ${threshold_pct}%\n\n`; + body += '| Metric | Baseline | Current | Change | Status |\n'; + body += '|--------|----------|---------|--------|--------|\n'; + + for (const c of checks) { + if (c.status === 'skip') { + body += `| ${c.label} | โ€” | โ€” | โ€” | โญ skip |\n`; + } else { + const change = `${c.change_pct > 0 ? '+' : ''}${c.change_pct}%`; + const status = c.status === 'pass' ? 'โœ…' : 'โŒ'; + body += `| ${c.label} | ${c.baseline} | ${c.current} | ${change} | ${status} |\n`; + } + } + } catch (e) { + body += '_Report not available._\n'; + } + + // Find and update existing comment, or create new one + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + const existing = comments.find(c => c.body.includes('Performance Regression Report')); + + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); + } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..d423ae85 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,211 @@ +name: Release + +on: + push: + branches: [main] + workflow_dispatch: + inputs: + deploy: + description: 'Deploy to stores (yes|no)' + required: true + default: 'no' + +permissions: + contents: write + issues: write + pull-requests: write + +jobs: + version-bump: + name: Version Bump & Changelog + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + outputs: + new_version: ${{ steps.version.outputs.new_version }} + version_changed: ${{ steps.version.outputs.version_changed }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Bump version and generate changelog + id: version + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Get current version + CURRENT_VERSION=$(node -p "require('./package.json').version") + echo "Current version: $CURRENT_VERSION" + + # Get commits since last tag + LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + if [ -z "$LAST_TAG" ]; then + COMMITS=$(git log --pretty=format:"%s" --no-merges) + else + COMMITS=$(git log ${LAST_TAG}..HEAD --pretty=format:"%s" --no-merges) + fi + + echo "Commits since last tag:" + echo "$COMMITS" + + # Determine version bump type + if echo "$COMMITS" | grep -qE "^(feat|feature)(\(.*\))?!:"; then + BUMP="minor" + elif echo "$COMMITS" | grep -qE "^(fix|bug)(\(.*\))?!:"; then + BUMP="patch" + elif echo "$COMMITS" | grep -qE "BREAKING CHANGE"; then + BUMP="major" + elif echo "$COMMITS" | grep -qE "^(feat|feature)(\(.*\))?:."; then + BUMP="minor" + elif echo "$COMMITS" | grep -qE "^(fix|bug)(\(.*\))?:."; then + BUMP="patch" + else + echo "No version-worthy changes detected" + echo "version_changed=false" >> $GITHUB_OUTPUT + exit 0 + fi + + echo "Bump type: $BUMP" + + # Bump version + NEW_VERSION=$(npm version $BUMP --no-git-tag-version) + NEW_VERSION=${NEW_VERSION#v} + echo "New version: $NEW_VERSION" + echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT + echo "version_changed=true" >> $GITHUB_OUTPUT + + # Generate changelog + npm install -g conventional-changelog-cli + conventional-changelog -p angular -i CHANGELOG.md -s -r 0 || conventional-changelog -p angular -i CHANGELOG.md -s + + # Commit and push + git add package.json CHANGELOG.md + git commit -m "chore(release): v$NEW_VERSION [skip ci]" || echo "No changes to commit" + git push origin main + + create-github-release: + name: Create GitHub Release + needs: version-bump + if: needs.version-bump.outputs.version_changed == 'true' + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get version + id: get_version + run: | + VERSION=${{ needs.version-bump.outputs.new_version }} + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Generate release notes + id: release_notes + run: | + LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + if [ -z "$LAST_TAG" ]; then + NOTES=$(git log --pretty=format:"* %s" --no-merges) + else + NOTES=$(git log ${LAST_TAG}..HEAD --pretty=format:"* %s" --no-merges) + fi + echo "NOTES<> $GITHUB_OUTPUT + echo "$NOTES" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ steps.get_version.outputs.version }} + name: Release v${{ steps.get_version.outputs.version }} + body: | + ## What's Changed + + ${{ steps.release_notes.outputs.NOTES }} + + **Full Changelog**: https://github.com/${{ github.repository }}/compare/${{ github.event.before }}...v${{ steps.get_version.outputs.version }} + draft: false + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + eas-build: + name: EAS Build (Production) + needs: [version-bump, create-github-release] + if: always() && needs.version-bump.outputs.version_changed == 'true' + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: main + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + + - name: Setup EAS + uses: expo/expo-github-action@v8 + with: + expo-version: latest + eas-version: latest + token: ${{ secrets.EXPO_TOKEN }} + + - name: Install dependencies + run: npm ci + + - name: Build on EAS (Android & iOS) + run: eas build --platform all --profile production --non-interactive --no-wait + + submit-to-stores: + name: Submit to App Stores + needs: eas-build + if: ${{ github.event.inputs.deploy == 'yes' }} + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + + - name: Setup EAS + uses: expo/expo-github-action@v8 + with: + expo-version: latest + eas-version: latest + token: ${{ secrets.EXPO_TOKEN }} + + - name: Install dependencies + run: npm ci + + - name: Submit to Google Play + run: eas submit --platform android --profile production --non-interactive + env: + EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} + + - name: Submit to App Store + run: eas submit --platform ios --profile production --non-interactive + env: + EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} diff --git a/.github/workflows/syntax.yml b/.github/workflows/syntax.yml new file mode 100644 index 00000000..6ee95f78 --- /dev/null +++ b/.github/workflows/syntax.yml @@ -0,0 +1,34 @@ +name: Syntax Gate + +on: + pull_request: + types: [opened, synchronize] + +concurrency: + group: syntax-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + syntax: + name: Syntax & Type Check + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + + - name: Install dependencies + run: npm ci --prefer-offline --no-audit --legacy-peer-deps + + - name: TypeScript type check + run: npx tsc --noEmit + + - name: ESLint check (no-fix) + run: npx eslint . --max-warnings=0 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..5de6d896 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,89 @@ +name: Test Suite + +on: + push: + branches: [main] + pull_request: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 15 + + env: + EXPO_PUBLIC_API_BASE_URL: https://api.teachlink.com + EXPO_PUBLIC_SOCKET_URL: wss://api.teachlink.com + EXPO_PUBLIC_APP_ENV: production + EXPO_PUBLIC_ENABLE_PUSH_NOTIFICATIONS: true + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + # ============================== + # ๐Ÿ“ฆ DEPENDENCY CACHING + # ============================== + - name: Cache node_modules + id: cache-deps + uses: actions/cache@v4 + with: + path: node_modules + key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + + - name: Install dependencies + if: steps.cache-deps.outputs.cache-hit != 'true' + run: npm ci --prefer-offline --no-audit + + # ============================== + # ๐Ÿงช JEST CACHE + # ============================== + - name: Cache Jest + uses: actions/cache@v4 + with: + path: | + .jest-cache + coverage + key: ${{ runner.os }}-jest-${{ hashFiles('jest.config.js', 'package-lock.json') }} + restore-keys: | + ${{ runner.os }}-jest- + + # ============================== + # ๐Ÿงช RUN TESTS + # ============================== + - name: Run unit tests + run: npm test -- --runInBand --testPathIgnorePatterns=perf --cache --cacheDirectory=.jest-cache + + - name: Run performance regression tests + run: npm test -- --testPathPattern=perf --runInBand --verbose --cache --cacheDirectory=.jest-cache + + # ============================== + # ๐Ÿ“Š COVERAGE REPORT + # ============================== + - name: Upload coverage to artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: coverage/ + retention-days: 7 + + - name: Generate test summary + if: always() + run: | + echo "## ๐Ÿงช Test Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "โœ… All tests passed!" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Cache Hit:** ${{ steps.cache-deps.outputs.cache-hit == 'true' && 'โœ… Yes' || 'โŒ No' }}" >> $GITHUB_STEP_SUMMARY \ No newline at end of file