From 69ac8d154b8e1898a2af006d932163ce3b4ad267 Mon Sep 17 00:00:00 2001 From: Philippe Ndiaye Date: Wed, 12 Aug 2026 11:13:38 +0200 Subject: [PATCH 1/7] Shareable web baseline (ours) config --- package.json | 3 ++- src/browsers/web-baseline.json | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 src/browsers/web-baseline.json diff --git a/package.json b/package.json index ea1f91f..efa1aff 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ ], "exports": { "./prettier": "./src/prettier/index.mjs", - "./eslint": "./src/eslint/index.mjs" + "./eslint": "./src/eslint/index.mjs", + "./browsers/web-baseline.json": "./src/browsers/web-baseline.json" } } diff --git a/src/browsers/web-baseline.json b/src/browsers/web-baseline.json new file mode 100644 index 0000000..fc60b31 --- /dev/null +++ b/src/browsers/web-baseline.json @@ -0,0 +1,9 @@ +[ + "chrome >= 105", + "edge >= 105", + "firefox >= 104", + "safari >= 15.6", + "ios_saf >= 15.6", + "and_chr >= 105", + "and_ff >= 104" +] From 8f522171a2f4c49e8ac4be962222c550d354a54a Mon Sep 17 00:00:00 2001 From: Philippe Ndiaye Date: Wed, 12 Aug 2026 11:16:03 +0200 Subject: [PATCH 2/7] gst --- .github/pull_request_template.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..29749cf --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,25 @@ +### What does this PR do? + + + +Related to: # + +### What are the observable changes? + + + + + + + + +### Good PR checklist + +- [ ] Title makes sense +- [ ] Is against the correct branch +- [ ] Only addresses one issue +- [ ] Properly assigned +- [ ] Added/updated tests +- [ ] Added/updated documentation with Figma design link. Don't forget to replace "design" by "file" in the URL. For example https://www.figma.com/file/example +- [ ] Migrated touched components to Glimmer Components +- [ ] Properly labeled From e22c8f10a417358d2c908a86e695acf95777bdaa Mon Sep 17 00:00:00 2001 From: Philippe Ndiaye Date: Wed, 12 Aug 2026 11:16:26 +0200 Subject: [PATCH 3/7] readme update --- .github/pull_request_template.md | 3 +-- README.md | 34 ++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 29749cf..173cf4d 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -20,6 +20,5 @@ Related to: # - [ ] Only addresses one issue - [ ] Properly assigned - [ ] Added/updated tests -- [ ] Added/updated documentation with Figma design link. Don't forget to replace "design" by "file" in the URL. For example https://www.figma.com/file/example -- [ ] Migrated touched components to Glimmer Components +- [ ] Added/updated documentation - [ ] Properly labeled diff --git a/README.md b/README.md index 90f1fc3..5cf9bf4 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,40 @@ pnpm add -D @upfluence/w-conf You can extend your project's configuration files with the shared configurations provided by `@upfluence/w-conf`. +### Browsers + +The shared browser target baseline is exported as JSON: + +```js +const browsers = require('@upfluence/w-conf/browsers/web-baseline.json'); +``` + +It is intended for Ember `config/targets.js` files: + +```js +'use strict'; + +const browsers = require('@upfluence/w-conf/browsers/web-baseline.json'); + +module.exports = { browsers }; +``` + +The current baseline matches Web Platform Baseline 2022-era browser support: + +```json +[ + "chrome >= 105", + "edge >= 105", + "firefox >= 104", + "safari >= 15.6", + "ios_saf >= 15.6", + "and_chr >= 105", + "and_ff >= 104" +] +``` + +This removes legacy targets such as IE 11 while keeping a stable, shared target set across Ember apps, addons, engines, and host apps. + ### Prettier The base Prettier configuration includes sensible defaults for code formatting that align with Upfluence's coding standards, including settings for Handlebars files and import sorting. From 1f3e8bcf60c3a45e8094d79b8ec2cc55538277b3 Mon Sep 17 00:00:00 2001 From: Philippe Ndiaye Date: Wed, 12 Aug 2026 20:29:33 +0200 Subject: [PATCH 4/7] rely on baseline widely available with downstream instead of specific versions list + add CI-controled automated checks --- .github/workflows/ci.yml | 28 ++ README.md | 65 ++- package.json | 8 + pnpm-lock.yaml | 600 ++++++++++++++++++++++++ scripts/check-web-baseline-coverage.mjs | 529 +++++++++++++++++++++ src/browsers/web-baseline-policy.json | 9 + src/browsers/web-baseline.json | 9 +- 7 files changed, 1228 insertions(+), 20 deletions(-) create mode 100644 scripts/check-web-baseline-coverage.mjs create mode 100644 src/browsers/web-baseline-policy.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2bcd9f2..18edf9a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,7 @@ on: tags: - v* pull_request: + workflow_dispatch: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -16,6 +17,33 @@ permissions: packages: write jobs: + web-baseline: + name: Web Baseline Coverage + runs-on: ubuntu-24.04 + timeout-minutes: 10 + env: + GA_PROPERTY_ID: ${{ secrets.GA_PROPERTY_ID }} + GOOGLE_APPLICATION_CREDENTIALS_JSON_BASE64: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS_JSON_BASE64 }} + steps: + - uses: actions/checkout@v3 + - uses: pnpm/action-setup@v4 + with: + version: 10 + - uses: actions/setup-node@v3 + with: + node-version: '${{ env.NODE_VERSION }}' + cache: 'pnpm' + - name: Install Dependencies + run: pnpm install + - name: Validate Web Baseline Target + run: pnpm web-baseline:check:target + - name: Check Web Baseline Traffic Coverage + if: env.GA_PROPERTY_ID != '' && env.GOOGLE_APPLICATION_CREDENTIALS_JSON_BASE64 != '' + run: pnpm web-baseline:check + - name: Skip Web Baseline Traffic Coverage + if: env.GA_PROPERTY_ID == '' || env.GOOGLE_APPLICATION_CREDENTIALS_JSON_BASE64 == '' + run: echo "Skipping GA4 traffic coverage check because GA secrets are not configured." + release: name: Release Package runs-on: ubuntu-24.04 diff --git a/README.md b/README.md index 5cf9bf4..f01aed7 100644 --- a/README.md +++ b/README.md @@ -30,21 +30,60 @@ const browsers = require('@upfluence/w-conf/browsers/web-baseline.json'); module.exports = { browsers }; ``` -The current baseline matches Web Platform Baseline 2022-era browser support: +The current baseline uses Web Platform Baseline Widely Available, including +compatible downstream browsers, with an explicit iOS Safari safety floor: ```json -[ - "chrome >= 105", - "edge >= 105", - "firefox >= 104", - "safari >= 15.6", - "ios_saf >= 15.6", - "and_chr >= 105", - "and_ff >= 104" -] -``` - -This removes legacy targets such as IE 11 while keeping a stable, shared target set across Ember apps, addons, engines, and host apps. +["baseline widely available with downstream", "ios_saf >= 15.6"] +``` + +`baseline widely available with downstream` tracks features that are broadly +available across the Baseline core browser set and includes compatible +downstream browsers, such as browsers derived from Chromium or Gecko. + +`ios_saf >= 15.6` is intentionally added as a mobile safety rail. Based on our +traffic analysis, older iOS/Safari versions are the main unsupported pocket, and +mobile Safari traffic is important enough for us to keep this explicit floor. + +This removes legacy targets such as IE 11 while keeping a stable, shared target +set across Ember apps, addons, engines, and host apps. + +Consumer projects need a Browserslist toolchain that supports Baseline queries. +If a build fails with `Unknown browser query`, update the consumer's +Browserslist-related dependencies before using this shared target. + +#### Web Baseline Coverage Check + +`w-conf` also ships a repository-level check that compares the shared browser +target against real Google Analytics traffic: + +```bash +pnpm web-baseline:check +``` + +The GA-backed check uses a 180-day window, `Sessions` as the primary metric, and +fails when known-session coverage drops below 95% or unknown traffic rises above +1%. + +For local CSV exports matching the GA browser export shape: + +```bash +pnpm web-baseline:check:csv /path/to/analytics-export.csv +``` + +To validate that the configured target can be parsed by the bundled Browserslist +version: + +```bash +pnpm web-baseline:check:target +``` + +GA4 mode requires a property ID and a base64-encoded service-account JSON value: + +```bash +export GA_PROPERTY_ID=123456789 +export GOOGLE_APPLICATION_CREDENTIALS_JSON_BASE64="$(base64 -i service-account.json)" +``` ### Prettier diff --git a/package.json b/package.json index efa1aff..8262da7 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,16 @@ "engines": { "node": ">=20.0.0" }, + "scripts": { + "web-baseline:check": "node scripts/check-web-baseline-coverage.mjs", + "web-baseline:check:csv": "node scripts/check-web-baseline-coverage.mjs --csv", + "web-baseline:check:target": "node scripts/check-web-baseline-coverage.mjs --check-target-only" + }, "devDependencies": { + "@google-analytics/data": "^7.0.0", "@trivago/prettier-plugin-sort-imports": "^5.2.2", + "baseline-browser-mapping": "^2.11.13", + "browserslist": "^4.28.8", "eslint": "^10.5.0", "prettier": "^3.6.2", "typescript": "^6.0.3" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c3072a2..2b5da9c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,9 +30,18 @@ importers: specifier: ^8.62.0 version: 8.62.0(eslint@10.5.0)(typescript@6.0.3) devDependencies: + '@google-analytics/data': + specifier: ^7.0.0 + version: 7.0.0 '@trivago/prettier-plugin-sort-imports': specifier: ^5.2.2 version: 5.2.2(prettier@3.8.1) + baseline-browser-mapping: + specifier: ^2.11.13 + version: 2.11.13 + browserslist: + specifier: ^4.28.8 + version: 4.28.8 eslint: specifier: ^10.5.0 version: 10.5.0 @@ -148,6 +157,19 @@ packages: '@glimmer/wire-format@0.94.8': resolution: {integrity: sha512-A+Cp5m6vZMAEu0Kg/YwU2dJZXyYxVJs2zI57d3CP6NctmX7FsT8WjViiRUmt5abVmMmRH5b8BUovqY6GSMAdrw==} + '@google-analytics/data@7.0.0': + resolution: {integrity: sha512-pAucE4Ek6i6KDprqYXioAhKl2aiKUuM6i2IThOtZV/Andr95+XF3fBsUW67MgT1a+LcwJTCCJLhabzZWGDp9pA==} + engines: {node: '>=22'} + + '@grpc/grpc-js@1.14.4': + resolution: {integrity: sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==} + engines: {node: '>=12.10.0'} + + '@grpc/proto-loader@0.8.1': + resolution: {integrity: sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==} + engines: {node: '>=6'} + hasBin: true + '@handlebars/parser@2.2.2': resolution: {integrity: sha512-n/SZW+12rwikx/f8YcSv9JCi5p9vn1Bnts9ZtVvfErG4h0gbjHI1H1ZMhVUnaOC7yzFc6PtsCKIK8XeTnL90Gw==} engines: {node: ^18 || ^20 || ^22 || >=24} @@ -185,6 +207,9 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@js-sdsl/ordered-map@4.4.2': + resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + '@napi-rs/wasm-runtime@1.1.5': resolution: {integrity: sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==} peerDependencies: @@ -324,6 +349,33 @@ packages: '@oxc-project/types@0.130.0': resolution: {integrity: sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==} + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.5': + resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} + + '@protobufjs/eventemitter@1.1.1': + resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} + + '@protobufjs/fetch@1.1.1': + resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.2': + resolution: {integrity: sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==} + '@simple-dom/interface@1.4.0': resolution: {integrity: sha512-l5qumKFWU0S+4ZzMaLXFU8tQZsicHEMEyAxI5kDFGhJsRqDwe0a7/iPA/GdxlGyDKseQQAgIz5kzU7eXTrlSpA==} @@ -355,6 +407,9 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/node@26.2.0': + resolution: {integrity: sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==} + '@typescript-eslint/eslint-plugin@8.62.0': resolution: {integrity: sha512-o+mpz7EYiMzXoySXiKmzlabIvTVqUuK5yLrAedRPRDA0IpPFMUV1IXt6OqljIxX/kumN6EjUYp41Hqelh6p/Dw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -424,9 +479,21 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + ajv@6.15.0: resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + aria-query@5.3.2: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} @@ -439,10 +506,43 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.11.13: + resolution: {integrity: sha512-k9HNuUVMlqVjQ9UHzfPjIqiDbWw7WqT1AoT7GL8VwvF3r0ZfArtgiSPAlmupyNquNgOJHTuH4CKYf8ttMTWBTQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + brace-expansion@5.0.6: resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} engines: {node: 18 || 20 || >=22} + browserslist@4.28.8: + resolution: {integrity: sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + caniuse-lite@1.0.30001809: + resolution: {integrity: sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + commander@14.0.3: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} @@ -458,6 +558,10 @@ packages: resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -473,11 +577,20 @@ packages: dot-case@3.0.4: resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} + duplexify@4.1.3: + resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + editorconfig@3.0.2: resolution: {integrity: sha512-T0ix8GhtxyKVfUFEcvdNDt3YGqlwkFHbD4/5bgFUDgFmxhI/cSRAeJ87/Sz//Cq8Eam6JX/e23RkoFO71P7aAA==} engines: {node: '>=20'} hasBin: true + electron-to-chromium@1.5.405: + resolution: {integrity: sha512-bNglH7lPH5l+yHOes7Zr4VqxhOy4BQ9ZBUX4VdoFgxMpzJk7W1ZoO3Vgd9Pxa9PyjQ76sfm2aKH/nzEcCNRlew==} + ember-eslint-parser@0.13.0: resolution: {integrity: sha512-i8mt96+yxFQaTNcz/2+SAkwpeLYU+VOFyHBshRyNL3HOciZhPpX3WszJK0/9GhVi8hQVGNtt21Iv7tsui3x0cQ==} engines: {node: '>=16.0.0'} @@ -496,10 +609,20 @@ packages: ember-rfc176-data@0.3.18: resolution: {integrity: sha512-JtuLoYGSjay1W3MQAxt3eINWXNYYQliK90tLwtb8aeCuQK8zKGCRbBodVIrkcTqshULMnRuTOS6t1P7oQk3g6Q==} + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + enhanced-resolve@5.24.0: resolution: {integrity: sha512-SkE2t82KlkkxQRVMVLAGKxLfORGQfrkx5dkj+vlgXRVNEdPc4eZcR+J/Fvj8C+yKSFH5L0q3NFlyufOVQnCcYQ==} engines: {node: '>=10.13.0'} + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -603,6 +726,9 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -621,6 +747,10 @@ packages: picomatch: optional: true + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -636,6 +766,22 @@ packages: flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + + gaxios@7.3.0: + resolution: {integrity: sha512-RB5vLV+vvQeoFPCX4QMK6/hjVkbIamPp1QSUD0CiZcnj12qbpiL+pLbYtgD+oZkWl0tl9z+o2Utp+MpM3QRhBA==} + engines: {node: '>=18'} + + gcp-metadata@9.0.2: + resolution: {integrity: sha512-zzoIz7qKIxyDp7Zghb12kl+n5Y1ofqYX97O/5MelKuFuDolvmT9RuIBGpEnHjqS4GBzPgMZFqH+WSUafRkn5dQ==} + engines: {node: '>=22'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + get-tsconfig@4.14.0: resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} @@ -654,6 +800,18 @@ packages: globrex@0.1.2: resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + google-auth-library@11.0.1: + resolution: {integrity: sha512-ZqfaYduu9ASUaFuUk5dF9g9QvufdhhSj7jFiEnCrTQcH57sFPKYetM0iU4dcKkQk6CqC1xpSrVr5uQ9NhqjNOg==} + engines: {node: '>=22'} + + google-gax@6.0.1: + resolution: {integrity: sha512-alzECOlNct1KTBeL9PQ6GIxJHvUQAp1MtffaSMx5+8f4hGRoDaET9oIWbHUtzfrFXRUsf6lnznum09yAKhU7AA==} + engines: {node: '>=22'} + + google-logging-utils@2.0.1: + resolution: {integrity: sha512-HMhaQghlOTvbcb3c4T5jmmOMtG3JUF1iOQMezaJXL86CDS+Tm2vHd0IeLFRAx3+ewd+bo9E1HFHoy17X5aJa9A==} + engines: {node: '>=22'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -665,6 +823,14 @@ packages: resolution: {integrity: sha512-n6l5uca7/y5joxZ3LUePhzmBFUJ+U2YWzhMa8XUTecSeSlQiZdF5XAd/Q3/WUl0VsXgUwWi8I7CNIwdI5WN1SQ==} engines: {node: '>=20.10'} + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -677,10 +843,17 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -699,6 +872,9 @@ packages: engines: {node: '>=6'} hasBin: true + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -708,6 +884,12 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -735,6 +917,9 @@ packages: lodash@4.17.23: resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} @@ -757,6 +942,26 @@ packages: no-case@3.0.4: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + node-releases@2.0.53: + resolution: {integrity: sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==} + engines: {node: '>=18'} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -797,10 +1002,26 @@ packages: engines: {node: '>=14'} hasBin: true + proto3-json-serializer@4.0.1: + resolution: {integrity: sha512-Y7YP6oERw5XsP+eFw0Pxe2DPjC6/lAW/ktxAKX+Sgb9fAWV4sl6hHIrBtq4taTkD7x3wLpgPS9ACn/czMp9rbw==} + engines: {node: '>=22'} + + protobufjs@7.6.5: + resolution: {integrity: sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==} + engines: {node: '>=12.0.0'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + requireindex@1.2.0: resolution: {integrity: sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==} engines: {node: '>=0.10.5'} @@ -808,6 +1029,13 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + retry-request@9.0.1: + resolution: {integrity: sha512-4kGHEBl0ME6gR+8QB1sPMyg58jUxLUcCZNeqLDrfqzk0eWQN8XJFmeu7fmZCjlZCRW0S2u64Ik1HkO/0sWgCYQ==} + engines: {node: '>=22'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + semver@7.8.5: resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} @@ -831,6 +1059,26 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + stream-events@1.0.5: + resolution: {integrity: sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==} + + stream-shift@1.0.3: + resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + stubs@3.0.0: + resolution: {integrity: sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==} + svg-tags@1.0.0: resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==} @@ -838,6 +1086,10 @@ packages: resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} + teeny-request@11.0.1: + resolution: {integrity: sha512-bNr5j2YjSdajgCVsp+8JVjRf1uHIbpNISLeKp/7V1NnVMX3Gh6H/kECNGlm2Q3I68ACODQ0iv6aZ3Rvm8WgLGA==} + engines: {node: '>=22'} + tinyglobby@0.2.17: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} @@ -871,9 +1123,25 @@ packages: engines: {node: '>=14.17'} hasBin: true + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + + update-browserslist-db@1.3.1: + resolution: {integrity: sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -883,6 +1151,25 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.3: + resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} + engines: {node: '>=12'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -1011,6 +1298,24 @@ snapshots: dependencies: '@glimmer/interfaces': 0.94.6 + '@google-analytics/data@7.0.0': + dependencies: + google-gax: 6.0.1 + transitivePeerDependencies: + - supports-color + + '@grpc/grpc-js@1.14.4': + dependencies: + '@grpc/proto-loader': 0.8.1 + '@js-sdsl/ordered-map': 4.4.2 + + '@grpc/proto-loader@0.8.1': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.6.5 + yargs: 17.7.3 + '@handlebars/parser@2.2.2': {} '@humanfs/core@0.19.2': @@ -1043,6 +1348,8 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@js-sdsl/ordered-map@4.4.2': {} + '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: '@emnapi/core': 1.10.0 @@ -1118,6 +1425,26 @@ snapshots: '@oxc-project/types@0.130.0': {} + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.5': {} + + '@protobufjs/eventemitter@1.1.1': {} + + '@protobufjs/fetch@1.1.1': + dependencies: + '@protobufjs/aspromise': 1.1.2 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.2': {} + '@simple-dom/interface@1.4.0': {} '@trivago/prettier-plugin-sort-imports@5.2.2(prettier@3.8.1)': @@ -1143,6 +1470,10 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/node@26.2.0': + dependencies: + undici-types: 8.3.0 + '@typescript-eslint/eslint-plugin@8.62.0(@typescript-eslint/parser@8.62.0(eslint@10.5.0)(typescript@6.0.3))(eslint@10.5.0)(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -1240,6 +1571,8 @@ snapshots: acorn@8.17.0: {} + agent-base@7.1.4: {} + ajv@6.15.0: dependencies: fast-deep-equal: 3.1.3 @@ -1247,16 +1580,52 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + aria-query@5.3.2: {} axobject-query@4.1.0: {} balanced-match@4.0.4: {} + base64-js@1.5.1: {} + + baseline-browser-mapping@2.11.13: {} + + bignumber.js@9.3.1: {} + brace-expansion@5.0.6: dependencies: balanced-match: 4.0.4 + browserslist@4.28.8: + dependencies: + baseline-browser-mapping: 2.11.13 + caniuse-lite: 1.0.30001809 + electron-to-chromium: 1.5.405 + node-releases: 2.0.53 + update-browserslist-db: 1.3.1(browserslist@4.28.8) + + buffer-equal-constant-time@1.0.1: {} + + caniuse-lite@1.0.30001809: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + commander@14.0.3: {} content-tag@4.2.0: {} @@ -1272,6 +1641,8 @@ snapshots: mdn-data: 2.27.1 source-map-js: 1.2.1 + data-uri-to-buffer@4.0.1: {} + debug@4.4.3: dependencies: ms: 2.1.3 @@ -1283,6 +1654,17 @@ snapshots: no-case: 3.0.4 tslib: 2.8.1 + duplexify@4.1.3: + dependencies: + end-of-stream: 1.4.5 + inherits: 2.0.4 + readable-stream: 3.6.2 + stream-shift: 1.0.3 + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + editorconfig@3.0.2: dependencies: '@one-ini/wasm': 0.2.1 @@ -1290,6 +1672,8 @@ snapshots: minimatch: 10.2.5 semver: 7.8.5 + electron-to-chromium@1.5.405: {} + ember-eslint-parser@0.13.0(@typescript-eslint/parser@8.62.0(eslint@10.5.0)(typescript@6.0.3))(typescript@6.0.3): dependencies: '@glimmer/syntax': 0.95.0 @@ -1314,11 +1698,19 @@ snapshots: ember-rfc176-data@0.3.18: {} + emoji-regex@8.0.0: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + enhanced-resolve@5.24.0: dependencies: graceful-fs: 4.2.11 tapable: 2.3.3 + escalade@3.2.0: {} + escape-string-regexp@4.0.0: {} eslint-compat-utils@0.5.1(eslint@10.5.0): @@ -1454,6 +1846,8 @@ snapshots: esutils@2.0.3: {} + extend@3.0.2: {} + fast-deep-equal@3.1.3: {} fast-json-stable-stringify@2.1.0: {} @@ -1464,6 +1858,11 @@ snapshots: optionalDependencies: picomatch: 4.0.4 + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -1480,6 +1879,28 @@ snapshots: flatted@3.4.2: {} + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + + gaxios@7.3.0: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + node-fetch: 3.3.2 + transitivePeerDependencies: + - supports-color + + gcp-metadata@9.0.2: + dependencies: + gaxios: 7.3.0 + google-logging-utils: 2.0.1 + json-bigint: 1.0.0 + transitivePeerDependencies: + - supports-color + + get-caller-file@2.0.5: {} + get-tsconfig@4.14.0: dependencies: resolve-pkg-maps: 1.0.0 @@ -1494,20 +1915,66 @@ snapshots: globrex@0.1.2: {} + google-auth-library@11.0.1: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 7.3.0 + gcp-metadata: 9.0.2 + google-logging-utils: 2.0.1 + jws: 4.0.1 + transitivePeerDependencies: + - supports-color + + google-gax@6.0.1: + dependencies: + '@grpc/grpc-js': 1.14.4 + '@grpc/proto-loader': 0.8.1 + duplexify: 4.1.3 + google-auth-library: 11.0.1 + google-logging-utils: 2.0.1 + node-fetch: 3.3.2 + object-hash: 3.0.0 + proto3-json-serializer: 4.0.1 + protobufjs: 7.6.5 + retry-request: 9.0.1 + transitivePeerDependencies: + - supports-color + + google-logging-utils@2.0.1: {} + graceful-fs@4.2.11: {} html-tags@3.3.1: {} html-tags@5.1.0: {} + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + ignore@5.3.2: {} ignore@7.0.5: {} imurmurhash@0.1.4: {} + inherits@2.0.4: {} + is-extglob@2.1.1: {} + is-fullwidth-code-point@3.0.0: {} + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 @@ -1520,12 +1987,27 @@ snapshots: jsesc@3.1.0: {} + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.3.1 + json-buffer@3.0.1: {} json-schema-traverse@0.4.1: {} json-stable-stringify-without-jsonify@1.0.1: {} + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -1551,6 +2033,8 @@ snapshots: lodash@4.17.23: {} + long@5.3.2: {} + lower-case@2.0.2: dependencies: tslib: 2.8.1 @@ -1572,6 +2056,22 @@ snapshots: lower-case: 2.0.2 tslib: 2.8.1 + node-domexception@1.0.0: {} + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + + node-releases@2.0.53: {} + + object-hash@3.0.0: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -1626,12 +2126,47 @@ snapshots: prettier@3.8.1: {} + proto3-json-serializer@4.0.1: + dependencies: + protobufjs: 7.6.5 + + protobufjs@7.6.5: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.5 + '@protobufjs/eventemitter': 1.1.1 + '@protobufjs/fetch': 1.1.1 + '@protobufjs/float': 1.0.2 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.2 + '@types/node': 26.2.0 + long: 5.3.2 + punycode@2.3.1: {} + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + require-directory@2.1.1: {} + requireindex@1.2.0: {} resolve-pkg-maps@1.0.0: {} + retry-request@9.0.1: + dependencies: + extend: 3.0.2 + teeny-request: 11.0.1 + transitivePeerDependencies: + - supports-color + + safe-buffer@5.2.1: {} + semver@7.8.5: {} shebang-command@2.0.0: @@ -1649,10 +2184,41 @@ snapshots: source-map-js@1.2.1: {} + stream-events@1.0.5: + dependencies: + stubs: 3.0.0 + + stream-shift@1.0.3: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + stubs@3.0.0: {} + svg-tags@1.0.0: {} tapable@2.3.3: {} + teeny-request@11.0.1: + dependencies: + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + node-fetch: 3.3.2 + stream-events: 1.0.5 + transitivePeerDependencies: + - supports-color + tinyglobby@0.2.17: dependencies: fdir: 6.5.0(picomatch@4.0.4) @@ -1683,14 +2249,48 @@ snapshots: typescript@6.0.3: {} + undici-types@8.3.0: {} + + update-browserslist-db@1.3.1(browserslist@4.28.8): + dependencies: + browserslist: 4.28.8 + escalade: 3.2.0 + picocolors: 1.1.1 + uri-js@4.4.1: dependencies: punycode: 2.3.1 + util-deprecate@1.0.2: {} + + web-streams-polyfill@3.3.3: {} + which@2.0.2: dependencies: isexe: 2.0.0 word-wrap@1.2.5: {} + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + y18n@5.0.8: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.3: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + yocto-queue@0.1.0: {} diff --git a/scripts/check-web-baseline-coverage.mjs b/scripts/check-web-baseline-coverage.mjs new file mode 100644 index 0000000..e6cc099 --- /dev/null +++ b/scripts/check-web-baseline-coverage.mjs @@ -0,0 +1,529 @@ +#!/usr/bin/env node + +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { BetaAnalyticsDataClient } from '@google-analytics/data'; +import { getCompatibleVersions } from 'baseline-browser-mapping'; +import browserslist from 'browserslist'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const root = path.resolve(__dirname, '..'); +const policyPath = path.join(root, 'src/browsers/web-baseline-policy.json'); +const analyticsScope = 'https://www.googleapis.com/auth/analytics.readonly'; +const baselineBrowserAliases = { + chrome: 'chrome', + chrome_android: 'and_chr', + edge: 'edge', + firefox: 'firefox', + firefox_android: 'and_ff', + opera: 'opera', + opera_android: 'op_mob', + safari: 'safari', + safari_ios: 'ios_saf', + samsunginternet_android: 'samsung', + webview_android: 'android' +}; + +const browserAliases = { + 'android webview': 'android', + chrome: 'chrome', + edge: 'edge', + firefox: 'firefox', + opera: 'opera', + safari: 'safari', + 'safari (in-app)': 'safari', + 'samsung internet': 'samsung' +}; + +main().catch((error) => { + console.error(`\nWeb baseline check failed: ${error.message}`); + process.exit(1); +}); + +async function main() { + const args = parseArgs(process.argv.slice(2)); + const policy = await readJson(policyPath); + const targetQueries = await readTargetQueries(policy); + const supportedTargets = resolveSupportedTargets(targetQueries); + + if (args.help) { + printHelp(); + return; + } + + if (args['check-target-only']) { + printTargetSummary(targetQueries, supportedTargets); + return; + } + + const source = await loadTrafficRows(args, policy); + const result = analyzeTraffic(source.rows, supportedTargets, policy); + + printReport({ + dateRange: source.dateRange, + policy, + result, + targetQueries + }); + + const knownCoverage = result.covered.sessions / result.known.sessions; + const unknownRate = result.unknown.sessions / result.total.sessions; + const failures = []; + + if (knownCoverage < policy.minimumSessionCoverage) { + failures.push( + `known session coverage ${formatPercent(knownCoverage)} is below ${formatPercent(policy.minimumSessionCoverage)}` + ); + } + + if (unknownRate > policy.maximumUnknownTraffic) { + failures.push( + `unknown session traffic ${formatPercent(unknownRate)} is above ${formatPercent(policy.maximumUnknownTraffic)}` + ); + } + + if (failures.length > 0) { + console.log('\nResult: FAIL'); + failures.forEach((failure) => console.log(`- ${failure}`)); + process.exit(1); + } + + console.log('\nResult: PASS'); +} + +function parseArgs(argv) { + const args = {}; + + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + + if (arg === '--help' || arg === '-h') { + args.help = true; + } else if (arg === '--csv') { + const next = argv[index + 1]; + if (next && !next.startsWith('--')) { + args.csv = next; + index += 1; + } else { + args.csv = process.env.WEB_BASELINE_CSV_PATH || true; + } + } else if (arg === '--check-target-only') { + args['check-target-only'] = true; + } else if (arg === '--start-date') { + args.startDate = argv[index + 1]; + index += 1; + } else if (arg === '--end-date') { + args.endDate = argv[index + 1]; + index += 1; + } else if (!args.csv && !arg.startsWith('--')) { + args.csv = arg; + } else { + throw new Error(`Unknown argument: ${arg}`); + } + } + + return args; +} + +function printHelp() { + console.log(` +Usage: + pnpm web-baseline:check + pnpm web-baseline:check:csv + node scripts/check-web-baseline-coverage.mjs --check-target-only + +Environment: + GA_PROPERTY_ID + GOOGLE_APPLICATION_CREDENTIALS_JSON_BASE64 + WEB_BASELINE_CSV_PATH +`); +} + +async function loadTrafficRows(args, policy) { + if (args.csv && args.csv !== true) { + return parseCsvExport(await readFile(path.resolve(args.csv), 'utf8')); + } + + if (args.csv === true) { + throw new Error('CSV mode needs a path argument or WEB_BASELINE_CSV_PATH.'); + } + + return fetchAnalyticsRows(args, policy); +} + +async function readJson(filePath) { + return JSON.parse(await readFile(filePath, 'utf8')); +} + +async function readTargetQueries(policy) { + const targetPath = path.resolve(path.dirname(policyPath), policy.target); + return readJson(targetPath); +} + +function resolveSupportedTargets(targetQueries) { + const targets = browserslist(targetQueries); + const minimums = new Map(); + + for (const target of getCompatibleVersions({ includeDownstreamBrowsers: true })) { + const browser = baselineBrowserAliases[target.browser]; + const candidate = parseVersion(target.version); + + if (!browser || !candidate) { + continue; + } + + mergeMinimum(minimums, browser, candidate); + } + + for (const query of targetQueries.filter((targetQuery) => !targetQuery.startsWith('baseline '))) { + for (const target of browserslist(query)) { + const [browser, version] = target.split(' '); + const candidate = parseVersion(version); + + if (!candidate) { + continue; + } + + mergeMinimum(minimums, browser, candidate); + } + } + + return { minimums, targets }; +} + +function mergeMinimum(minimums, browser, candidate) { + const current = minimums.get(browser); + + if (!current || compareVersions(candidate, current) < 0) { + minimums.set(browser, candidate); + } +} + +function printTargetSummary(targetQueries, supportedTargets) { + console.log('Web baseline target parsed successfully.'); + console.log(`Queries: ${targetQueries.join(', ')}`); + console.log('Resolved practical minimums:'); + + [...supportedTargets.minimums.entries()] + .sort(([left], [right]) => left.localeCompare(right)) + .forEach(([browser, version]) => { + console.log(`- ${browser} >= ${formatVersion(version)}`); + }); +} + +async function fetchAnalyticsRows(args, policy) { + const propertyId = process.env.GA_PROPERTY_ID; + const credentialsBase64 = process.env.GOOGLE_APPLICATION_CREDENTIALS_JSON_BASE64; + + if (!propertyId) { + throw new Error('GA_PROPERTY_ID is required for GA4 mode.'); + } + + if (!credentialsBase64) { + throw new Error('GOOGLE_APPLICATION_CREDENTIALS_JSON_BASE64 is required for GA4 mode.'); + } + + const credentials = JSON.parse(Buffer.from(credentialsBase64, 'base64').toString('utf8')); + const client = new BetaAnalyticsDataClient({ + credentials, + scopes: [analyticsScope] + }); + const endDate = args.endDate || 'yesterday'; + const startDate = args.startDate || `${policy.windowDays}daysAgo`; + const request = { + property: `properties/${propertyId}`, + dateRanges: [{ startDate, endDate }], + dimensions: [ + { name: 'browser' }, + { name: 'browserVersion' }, + { name: 'deviceCategory' }, + { name: 'operatingSystem' }, + { name: 'operatingSystemVersion' } + ], + metrics: [{ name: policy.primaryMetric }, { name: policy.secondaryMetric }], + limit: 250000 + }; + const [response] = await client.runReport(request); + + return { + dateRange: `${startDate} to ${endDate}`, + rows: (response.rows || []).map((row) => ({ + browser: row.dimensionValues[0]?.value || '', + browserVersion: row.dimensionValues[1]?.value || '', + deviceCategory: row.dimensionValues[2]?.value || '', + operatingSystem: row.dimensionValues[3]?.value || '', + osVersion: row.dimensionValues[4]?.value || '', + views: Number(row.metricValues[1]?.value || 0), + sessions: Number(row.metricValues[0]?.value || 0) + })) + }; +} + +function parseCsvExport(input) { + const lines = input.split(/\r?\n/).filter(Boolean); + const dateRange = lines.find((line) => /^#\s*\d{8}-\d{8}/.test(line))?.replace(/^#\s*/, '') || 'CSV export'; + const headerIndex = lines.findIndex((line) => line.startsWith('Browser,Browser version,')); + + if (headerIndex === -1) { + throw new Error('CSV header not found.'); + } + + const headers = parseCsvLine(lines[headerIndex]); + const rows = []; + + for (const line of lines.slice(headerIndex + 1)) { + const values = parseCsvLine(line); + const record = Object.fromEntries(headers.map((header, index) => [header, values[index] || ''])); + + if (values.includes('Grand total')) { + continue; + } + + rows.push({ + browser: record.Browser, + browserVersion: record['Browser version'], + deviceCategory: record['Device category'], + operatingSystem: record['Operating system'], + osVersion: record['OS version'], + views: parseMetric(record.Views), + sessions: parseMetric(record.Sessions) + }); + } + + return { dateRange, rows }; +} + +function parseCsvLine(line) { + const values = []; + let value = ''; + let quoted = false; + + for (let index = 0; index < line.length; index += 1) { + const char = line[index]; + const next = line[index + 1]; + + if (char === '"' && quoted && next === '"') { + value += '"'; + index += 1; + } else if (char === '"') { + quoted = !quoted; + } else if (char === ',' && !quoted) { + values.push(value); + value = ''; + } else { + value += char; + } + } + + values.push(value); + return values; +} + +function parseMetric(value) { + return Number(String(value || '0').replace(/,/g, '')); +} + +function analyzeTraffic(rows, supportedTargets, policy) { + const result = { + total: emptyMetrics(), + known: emptyMetrics(), + covered: emptyMetrics(), + unsupported: emptyMetrics(), + unknown: emptyMetrics(), + devices: new Map(), + unsupportedRows: [] + }; + + for (const row of rows) { + const metrics = { sessions: row.sessions, views: row.views }; + addMetrics(result.total, metrics); + + const classification = classifyRow(row, supportedTargets.minimums); + const device = normalize(row.deviceCategory) || 'unknown'; + const deviceResult = result.devices.get(device) || { + total: emptyMetrics(), + covered: emptyMetrics(), + unsupported: emptyMetrics(), + unknown: emptyMetrics() + }; + + result.devices.set(device, deviceResult); + addMetrics(deviceResult.total, metrics); + + if (classification.status === 'covered') { + addMetrics(result.known, metrics); + addMetrics(result.covered, metrics); + addMetrics(deviceResult.covered, metrics); + } else if (classification.status === 'unsupported') { + addMetrics(result.known, metrics); + addMetrics(result.unsupported, metrics); + addMetrics(deviceResult.unsupported, metrics); + result.unsupportedRows.push({ ...row, reason: classification.reason }); + } else { + addMetrics(result.unknown, metrics); + addMetrics(deviceResult.unknown, metrics); + } + } + + result.unsupportedRows.sort((left, right) => right.sessions - left.sessions); + return result; +} + +function classifyRow(row, minimums) { + const browser = normalize(row.browser); + const operatingSystem = normalize(row.operatingSystem); + + if (!browser || browser === '(not set)' || browser === 'mozilla compatible agent') { + return { status: 'unknown', reason: 'unmapped browser' }; + } + + const targetBrowser = resolveTargetBrowser(row, browser, operatingSystem); + const minimum = minimums.get(targetBrowser); + + if (!targetBrowser || !minimum) { + return { status: 'unknown', reason: `no target mapping for ${row.browser}` }; + } + + const version = targetBrowser === 'ios_saf' ? parseVersion(row.osVersion) : parseVersion(row.browserVersion); + + if (!version) { + return { status: 'unknown', reason: 'missing version' }; + } + + if (compareVersions(version, minimum) >= 0) { + return { status: 'covered' }; + } + + return { + status: 'unsupported', + reason: `${targetBrowser} ${formatVersion(version)} < ${formatVersion(minimum)}` + }; +} + +function resolveTargetBrowser(row, browser, operatingSystem) { + if (isIos(row, operatingSystem)) { + return 'ios_saf'; + } + + if (browser === 'chrome' && normalize(row.deviceCategory) === 'mobile') { + return 'and_chr'; + } + + if (browser === 'firefox' && normalize(row.deviceCategory) === 'mobile') { + return 'and_ff'; + } + + return browserAliases[browser]; +} + +function isIos(row, operatingSystem) { + return ( + operatingSystem === 'ios' || + operatingSystem === 'ipados' || + normalize(row.osVersion).startsWith('ios ') || + normalize(row.osVersion).startsWith('ipados ') + ); +} + +function parseVersion(value) { + const match = String(value || '').match(/\d+(?:\.\d+)*/); + + if (!match) { + return null; + } + + return match[0].split('.').map((part) => Number(part)); +} + +function compareVersions(left, right) { + const length = Math.max(left.length, right.length); + + for (let index = 0; index < length; index += 1) { + const leftPart = left[index] || 0; + const rightPart = right[index] || 0; + + if (leftPart !== rightPart) { + return leftPart - rightPart; + } + } + + return 0; +} + +function emptyMetrics() { + return { sessions: 0, views: 0 }; +} + +function addMetrics(target, source) { + target.sessions += source.sessions; + target.views += source.views; +} + +function printReport({ dateRange, policy, result, targetQueries }) { + const knownCoverage = result.covered.sessions / result.known.sessions; + const totalCoverage = result.covered.sessions / result.total.sessions; + const unknownRate = result.unknown.sessions / result.total.sessions; + + console.log('Web baseline coverage check'); + console.log(`Date range: ${dateRange}`); + console.log(`Target: ${targetQueries.join(', ')}`); + console.log(`Minimum known-session coverage: ${formatPercent(policy.minimumSessionCoverage)}`); + console.log(`Maximum unknown-session traffic: ${formatPercent(policy.maximumUnknownTraffic)}`); + console.log(''); + console.log('Sessions'); + console.log(`- total: ${formatNumber(result.total.sessions)}`); + console.log(`- covered: ${formatNumber(result.covered.sessions)} (${formatPercent(totalCoverage)} including unknown, ${formatPercent(knownCoverage)} excluding unknown)`); + console.log(`- unsupported: ${formatNumber(result.unsupported.sessions)}`); + console.log(`- unknown: ${formatNumber(result.unknown.sessions)} (${formatPercent(unknownRate)})`); + console.log(''); + console.log('Views'); + console.log(`- total: ${formatNumber(result.total.views)}`); + console.log(`- covered: ${formatNumber(result.covered.views)}`); + console.log(`- unsupported: ${formatNumber(result.unsupported.views)}`); + console.log(`- unknown: ${formatNumber(result.unknown.views)}`); + + if (policy.deviceBreakdown) { + console.log(''); + console.log('Device split'); + [...result.devices.entries()] + .sort(([left], [right]) => left.localeCompare(right)) + .forEach(([device, metrics]) => { + const covered = metrics.covered.sessions / metrics.total.sessions; + console.log( + `- ${device}: ${formatNumber(metrics.covered.sessions)} / ${formatNumber(metrics.total.sessions)} sessions covered (${formatPercent(covered)})` + ); + }); + } + + if (result.unsupportedRows.length > 0) { + console.log(''); + console.log('Top unsupported traffic'); + result.unsupportedRows.slice(0, 10).forEach((row) => { + console.log( + `- ${formatNumber(row.sessions)} sessions, ${formatNumber(row.views)} views: ${row.browser} ${row.browserVersion}, ${row.deviceCategory}, ${row.operatingSystem} ${row.osVersion} (${row.reason})` + ); + }); + } +} + +function normalize(value) { + return String(value || '').trim().toLowerCase(); +} + +function formatPercent(value) { + if (!Number.isFinite(value)) { + return '0.00%'; + } + + return `${(value * 100).toFixed(2)}%`; +} + +function formatNumber(value) { + return new Intl.NumberFormat('en-US').format(value); +} + +function formatVersion(version) { + return version.join('.'); +} diff --git a/src/browsers/web-baseline-policy.json b/src/browsers/web-baseline-policy.json new file mode 100644 index 0000000..f5610eb --- /dev/null +++ b/src/browsers/web-baseline-policy.json @@ -0,0 +1,9 @@ +{ + "target": "./web-baseline.json", + "primaryMetric": "sessions", + "secondaryMetric": "screenPageViews", + "windowDays": 180, + "minimumSessionCoverage": 0.95, + "maximumUnknownTraffic": 0.01, + "deviceBreakdown": true +} diff --git a/src/browsers/web-baseline.json b/src/browsers/web-baseline.json index fc60b31..b0f493c 100644 --- a/src/browsers/web-baseline.json +++ b/src/browsers/web-baseline.json @@ -1,9 +1,4 @@ [ - "chrome >= 105", - "edge >= 105", - "firefox >= 104", - "safari >= 15.6", - "ios_saf >= 15.6", - "and_chr >= 105", - "and_ff >= 104" + "baseline widely available with downstream", + "ios_saf >= 15.6" ] From 54c76f617f99d203c3a39079ebbbcf24bcebbdab Mon Sep 17 00:00:00 2001 From: Philippe Ndiaye Date: Mon, 17 Aug 2026 10:30:15 +0200 Subject: [PATCH 5/7] Update README.md Co-authored-by: Edouard <63284636+edouardmisset@users.noreply.github.com> --- README.md | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index f01aed7..f47c726 100644 --- a/README.md +++ b/README.md @@ -14,21 +14,15 @@ You can extend your project's configuration files with the shared configurations ### Browsers -The shared browser target baseline is exported as JSON: - -```js -const browsers = require('@upfluence/w-conf/browsers/web-baseline.json'); -``` - -It is intended for Ember `config/targets.js` files: +The shared browser target is exported as JSON. It is intended for use with Ember `config/targets.js` files: ```js +// config/targets.js 'use strict'; const browsers = require('@upfluence/w-conf/browsers/web-baseline.json'); module.exports = { browsers }; -``` The current baseline uses Web Platform Baseline Widely Available, including compatible downstream browsers, with an explicit iOS Safari safety floor: From a7b27d3a4826d3ce7404a7c38cdcadd3063d2413 Mon Sep 17 00:00:00 2001 From: Philippe Ndiaye Date: Mon, 17 Aug 2026 10:32:05 +0200 Subject: [PATCH 6/7] Update README.md Co-authored-by: Edouard <63284636+edouardmisset@users.noreply.github.com> --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index f47c726..a71c833 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,9 @@ Consumer projects need a Browserslist toolchain that supports Baseline queries. If a build fails with `Unknown browser query`, update the consumer's Browserslist-related dependencies before using this shared target. +`pnpm dlx update-browserslist-db latest` + + #### Web Baseline Coverage Check `w-conf` also ships a repository-level check that compares the shared browser From 513595414a3b87a274d2b1290213063b0fc976d7 Mon Sep 17 00:00:00 2001 From: Philippe Ndiaye Date: Tue, 18 Aug 2026 15:28:25 +0200 Subject: [PATCH 7/7] ensure fetched GA data validity and highlight resolved browsers' release dates --- .github/workflows/ci.yml | 2 + package.json | 3 +- scripts/check-web-baseline-coverage.mjs | 148 +++++++++++++++--- tests/web-baseline/coverage-test.mjs | 72 +++++++++ tests/web-baseline/fixtures/fail-empty.csv | 5 + tests/web-baseline/fixtures/fail-unknown.csv | 7 + .../fixtures/fail-unsupported.csv | 7 + tests/web-baseline/fixtures/pass.csv | 8 + 8 files changed, 232 insertions(+), 20 deletions(-) create mode 100644 tests/web-baseline/coverage-test.mjs create mode 100644 tests/web-baseline/fixtures/fail-empty.csv create mode 100644 tests/web-baseline/fixtures/fail-unknown.csv create mode 100644 tests/web-baseline/fixtures/fail-unsupported.csv create mode 100644 tests/web-baseline/fixtures/pass.csv diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 18edf9a..642dfbb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,6 +37,8 @@ jobs: run: pnpm install - name: Validate Web Baseline Target run: pnpm web-baseline:check:target + - name: Test Web Baseline Checker + run: pnpm web-baseline:test - name: Check Web Baseline Traffic Coverage if: env.GA_PROPERTY_ID != '' && env.GOOGLE_APPLICATION_CREDENTIALS_JSON_BASE64 != '' run: pnpm web-baseline:check diff --git a/package.json b/package.json index 8262da7..8460e74 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "scripts": { "web-baseline:check": "node scripts/check-web-baseline-coverage.mjs", "web-baseline:check:csv": "node scripts/check-web-baseline-coverage.mjs --csv", - "web-baseline:check:target": "node scripts/check-web-baseline-coverage.mjs --check-target-only" + "web-baseline:check:target": "node scripts/check-web-baseline-coverage.mjs --check-target-only", + "web-baseline:test": "node tests/web-baseline/coverage-test.mjs" }, "devDependencies": { "@google-analytics/data": "^7.0.0", diff --git a/scripts/check-web-baseline-coverage.mjs b/scripts/check-web-baseline-coverage.mjs index e6cc099..e4b003b 100644 --- a/scripts/check-web-baseline-coverage.mjs +++ b/scripts/check-web-baseline-coverage.mjs @@ -25,6 +25,20 @@ const baselineBrowserAliases = { samsunginternet_android: 'samsung', webview_android: 'android' }; +const coreBaselineBrowsers = new Set(['chrome', 'edge', 'firefox', 'safari', 'ios_saf']); +const browserDisplayNames = { + and_chr: 'Chrome Android', + and_ff: 'Firefox Android', + android: 'Android WebView', + chrome: 'Chrome', + edge: 'Edge', + firefox: 'Firefox', + ios_saf: 'iOS Safari', + op_mob: 'Opera Android', + opera: 'Opera', + safari: 'Safari', + samsung: 'Samsung Internet' +}; const browserAliases = { 'android webview': 'android', @@ -45,6 +59,7 @@ main().catch((error) => { async function main() { const args = parseArgs(process.argv.slice(2)); const policy = await readJson(policyPath); + validatePolicy(policy); const targetQueries = await readTargetQueries(policy); const supportedTargets = resolveSupportedTargets(targetQueries); @@ -65,12 +80,13 @@ async function main() { dateRange: source.dateRange, policy, result, - targetQueries + targetQueries, + supportedTargets }); + const failures = validateResult(result); const knownCoverage = result.covered.sessions / result.known.sessions; const unknownRate = result.unknown.sessions / result.total.sessions; - const failures = []; if (knownCoverage < policy.minimumSessionCoverage) { failures.push( @@ -93,6 +109,30 @@ async function main() { console.log('\nResult: PASS'); } +function validatePolicy(policy) { + if (policy.primaryMetric !== 'sessions') { + throw new Error('web-baseline-policy.json primaryMetric must be "sessions".'); + } + + if (policy.secondaryMetric !== 'screenPageViews') { + throw new Error('web-baseline-policy.json secondaryMetric must be "screenPageViews".'); + } +} + +function validateResult(result) { + const failures = []; + + if (result.total.sessions === 0) { + failures.push('traffic result contains zero total sessions'); + } + + if (result.known.sessions === 0) { + failures.push('traffic result contains zero known sessions'); + } + + return failures; +} + function parseArgs(argv) { const args = {}; @@ -165,6 +205,7 @@ async function readTargetQueries(policy) { function resolveSupportedTargets(targetQueries) { const targets = browserslist(targetQueries); const minimums = new Map(); + const minimumDetails = new Map(); for (const target of getCompatibleVersions({ includeDownstreamBrowsers: true })) { const browser = baselineBrowserAliases[target.browser]; @@ -174,7 +215,12 @@ function resolveSupportedTargets(targetQueries) { continue; } - mergeMinimum(minimums, browser, candidate); + mergeMinimum(minimums, minimumDetails, { + browser, + version: candidate, + releaseDate: target.release_date, + source: coreBaselineBrowsers.has(browser) ? 'Baseline Widely Available' : 'downstream' + }); } for (const query of targetQueries.filter((targetQuery) => !targetQuery.startsWith('baseline '))) { @@ -186,31 +232,31 @@ function resolveSupportedTargets(targetQueries) { continue; } - mergeMinimum(minimums, browser, candidate); + mergeMinimum(minimums, minimumDetails, { + browser, + version: candidate, + releaseDate: null, + source: 'explicit override' + }); } } - return { minimums, targets }; + return { minimumDetails, minimums, targets }; } -function mergeMinimum(minimums, browser, candidate) { - const current = minimums.get(browser); +function mergeMinimum(minimums, minimumDetails, detail) { + const current = minimums.get(detail.browser); - if (!current || compareVersions(candidate, current) < 0) { - minimums.set(browser, candidate); + if (!current || compareVersions(detail.version, current) < 0) { + minimums.set(detail.browser, detail.version); + minimumDetails.set(detail.browser, detail); } } function printTargetSummary(targetQueries, supportedTargets) { console.log('Web baseline target parsed successfully.'); console.log(`Queries: ${targetQueries.join(', ')}`); - console.log('Resolved practical minimums:'); - - [...supportedTargets.minimums.entries()] - .sort(([left], [right]) => left.localeCompare(right)) - .forEach(([browser, version]) => { - console.log(`- ${browser} >= ${formatVersion(version)}`); - }); + printBrowserMinimums(supportedTargets); } async function fetchAnalyticsRows(args, policy) { @@ -246,6 +292,7 @@ async function fetchAnalyticsRows(args, policy) { limit: 250000 }; const [response] = await client.runReport(request); + const metricIndexes = buildMetricIndexes(response.metricHeaders || []); return { dateRange: `${startDate} to ${endDate}`, @@ -255,12 +302,26 @@ async function fetchAnalyticsRows(args, policy) { deviceCategory: row.dimensionValues[2]?.value || '', operatingSystem: row.dimensionValues[3]?.value || '', osVersion: row.dimensionValues[4]?.value || '', - views: Number(row.metricValues[1]?.value || 0), - sessions: Number(row.metricValues[0]?.value || 0) + views: readMetricValue(row, metricIndexes, policy.secondaryMetric), + sessions: readMetricValue(row, metricIndexes, policy.primaryMetric) })) }; } +function buildMetricIndexes(metricHeaders) { + return new Map(metricHeaders.map((header, index) => [header.name, index])); +} + +function readMetricValue(row, metricIndexes, metricName) { + const index = metricIndexes.get(metricName); + + if (index === undefined) { + throw new Error(`GA4 response did not include metric "${metricName}".`); + } + + return Number(row.metricValues[index]?.value || 0); +} + function parseCsvExport(input) { const lines = input.split(/\r?\n/).filter(Boolean); const dateRange = lines.find((line) => /^#\s*\d{8}-\d{8}/.test(line))?.replace(/^#\s*/, '') || 'CSV export'; @@ -461,7 +522,7 @@ function addMetrics(target, source) { target.views += source.views; } -function printReport({ dateRange, policy, result, targetQueries }) { +function printReport({ dateRange, policy, result, supportedTargets, targetQueries }) { const knownCoverage = result.covered.sessions / result.known.sessions; const totalCoverage = result.covered.sessions / result.total.sessions; const unknownRate = result.unknown.sessions / result.total.sessions; @@ -471,6 +532,7 @@ function printReport({ dateRange, policy, result, targetQueries }) { console.log(`Target: ${targetQueries.join(', ')}`); console.log(`Minimum known-session coverage: ${formatPercent(policy.minimumSessionCoverage)}`); console.log(`Maximum unknown-session traffic: ${formatPercent(policy.maximumUnknownTraffic)}`); + printBrowserMinimums(supportedTargets); console.log(''); console.log('Sessions'); console.log(`- total: ${formatNumber(result.total.sessions)}`); @@ -508,6 +570,54 @@ function printReport({ dateRange, policy, result, targetQueries }) { } } +function printBrowserMinimums(supportedTargets) { + const rows = [...supportedTargets.minimumDetails.values()] + .sort((left, right) => getBrowserSortIndex(left.browser) - getBrowserSortIndex(right.browser)) + .map((detail) => ({ + Browser: browserDisplayNames[detail.browser] || detail.browser, + Minimum: formatVersion(detail.version), + Released: detail.releaseDate || 'override', + Period: detail.releaseDate ? formatReleasePeriod(detail.releaseDate) : getOverridePeriod(detail), + Source: detail.source + })); + + console.log(''); + console.log('Resolved browser minimums'); + printTable(['Browser', 'Minimum', 'Released', 'Period', 'Source'], rows); +} + +function printTable(headers, rows) { + const widths = headers.map((header) => { + return Math.max(header.length, ...rows.map((row) => String(row[header]).length)); + }); + const formatRow = (row) => { + return headers.map((header, index) => String(row[header]).padEnd(widths[index])).join(' '); + }; + + console.log(formatRow(Object.fromEntries(headers.map((header) => [header, header])))); + console.log(widths.map((width) => '-'.repeat(width)).join(' ')); + rows.forEach((row) => console.log(formatRow(row))); +} + +function getBrowserSortIndex(browser) { + return ['chrome', 'edge', 'firefox', 'safari', 'ios_saf', 'and_chr', 'and_ff', 'android', 'opera', 'op_mob', 'samsung'].indexOf(browser); +} + +function formatReleasePeriod(releaseDate) { + const date = new Date(`${releaseDate}T00:00:00Z`); + const quarter = Math.floor(date.getUTCMonth() / 3) + 1; + + return `Q${quarter} ${date.getUTCFullYear()}`; +} + +function getOverridePeriod(detail) { + if (detail.browser === 'ios_saf') { + return 'iOS safety floor'; + } + + return 'explicit override'; +} + function normalize(value) { return String(value || '').trim().toLowerCase(); } diff --git a/tests/web-baseline/coverage-test.mjs b/tests/web-baseline/coverage-test.mjs new file mode 100644 index 0000000..fdb88b9 --- /dev/null +++ b/tests/web-baseline/coverage-test.mjs @@ -0,0 +1,72 @@ +#!/usr/bin/env node + +import { spawnSync } from 'node:child_process'; +import assert from 'node:assert/strict'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const root = path.resolve(__dirname, '../..'); +const checker = path.join(root, 'scripts/check-web-baseline-coverage.mjs'); + +const cases = [ + { + name: 'target parses', + args: ['--check-target-only'], + expectedStatus: 0, + expectedOutput: [ + 'Web baseline target parsed successfully.', + 'Resolved browser minimums', + 'Chrome', + 'Q1 2024', + 'Safari', + 'Q4 2023', + 'iOS safety floor' + ] + }, + { + name: 'passing traffic passes', + args: ['--csv', 'tests/web-baseline/fixtures/pass.csv'], + expectedStatus: 0, + expectedOutput: 'Result: PASS' + }, + { + name: 'unsupported traffic fails', + args: ['--csv', 'tests/web-baseline/fixtures/fail-unsupported.csv'], + expectedStatus: 1, + expectedOutput: 'known session coverage 90.00% is below 95.00%' + }, + { + name: 'unknown traffic fails', + args: ['--csv', 'tests/web-baseline/fixtures/fail-unknown.csv'], + expectedStatus: 1, + expectedOutput: 'unknown session traffic 2.00% is above 1.00%' + }, + { + name: 'empty traffic fails', + args: ['--csv', 'tests/web-baseline/fixtures/fail-empty.csv'], + expectedStatus: 1, + expectedOutput: 'traffic result contains zero total sessions' + } +]; + +for (const testCase of cases) { + const result = spawnSync(process.execPath, [checker, ...testCase.args], { + cwd: root, + encoding: 'utf8' + }); + const output = `${result.stdout}\n${result.stderr}`; + + assert.equal(result.status, testCase.expectedStatus, `${testCase.name}: unexpected exit status\n${output}`); + const expectedOutputs = Array.isArray(testCase.expectedOutput) ? testCase.expectedOutput : [testCase.expectedOutput]; + + for (const expectedOutput of expectedOutputs) { + assert.match(output, new RegExp(escapeRegExp(expectedOutput)), `${testCase.name}: missing expected output\n${output}`); + } +} + +console.log(`web-baseline tests passed (${cases.length})`); + +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} diff --git a/tests/web-baseline/fixtures/fail-empty.csv b/tests/web-baseline/fixtures/fail-empty.csv new file mode 100644 index 0000000..752eedb --- /dev/null +++ b/tests/web-baseline/fixtures/fail-empty.csv @@ -0,0 +1,5 @@ +# Tech Chapter - Frontend +# Web baseline empty fixture +# 20260715-20260811 +Browser,Browser version,Device category,Operating system,OS version,Views,Sessions +Grand total,,,,,0,0 diff --git a/tests/web-baseline/fixtures/fail-unknown.csv b/tests/web-baseline/fixtures/fail-unknown.csv new file mode 100644 index 0000000..888de0f --- /dev/null +++ b/tests/web-baseline/fixtures/fail-unknown.csv @@ -0,0 +1,7 @@ +# Tech Chapter - Frontend +# Web baseline unknown-heavy fixture +# 20260715-20260811 +Browser,Browser version,Device category,Operating system,OS version,Views,Sessions +Grand total,,,,,1000,1000 +Chrome,130.0.0.0,desktop,Windows,11,980,980 +(not set),(not set),smart tv,(not set),(not set),20,20 diff --git a/tests/web-baseline/fixtures/fail-unsupported.csv b/tests/web-baseline/fixtures/fail-unsupported.csv new file mode 100644 index 0000000..8759e36 --- /dev/null +++ b/tests/web-baseline/fixtures/fail-unsupported.csv @@ -0,0 +1,7 @@ +# Tech Chapter - Frontend +# Web baseline unsupported-heavy fixture +# 20260715-20260811 +Browser,Browser version,Device category,Operating system,OS version,Views,Sessions +Grand total,,,,,1000,1000 +Chrome,130.0.0.0,desktop,Windows,11,900,900 +Chrome,99.0.0.0,desktop,Windows,10,100,100 diff --git a/tests/web-baseline/fixtures/pass.csv b/tests/web-baseline/fixtures/pass.csv new file mode 100644 index 0000000..73ad317 --- /dev/null +++ b/tests/web-baseline/fixtures/pass.csv @@ -0,0 +1,8 @@ +# Tech Chapter - Frontend +# Web baseline fixture +# 20260715-20260811 +Browser,Browser version,Device category,Operating system,OS version,Views,Sessions +Grand total,,,,,1000,1000 +Chrome,130.0.0.0,desktop,Windows,11,800,800 +Safari,16.5,mobile,iOS,15.7.2,180,180 +Chrome,99.0.0.0,desktop,Windows,10,20,20