From 61405a30360646bb7c5f5aafcaabe9c6c733d2f4 Mon Sep 17 00:00:00 2001 From: conico974 Date: Fri, 7 Aug 2026 15:44:52 +0200 Subject: [PATCH] feat: add CSS browser compatibility check for pull requests (#4478) --- .../workflows/css-browser-compatibility.yaml | 35 +++ bun.lock | 80 ++++- packages/gitbook/package.json | 3 + .../check-css-browser-compatibility.ts | 245 ++++++++++++++++ .../src/lib/cssBrowserCompatibility.test.ts | 210 ++++++++++++++ .../src/lib/cssBrowserCompatibility.ts | 274 ++++++++++++++++++ 6 files changed, 840 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/css-browser-compatibility.yaml create mode 100644 packages/gitbook/scripts/check-css-browser-compatibility.ts create mode 100644 packages/gitbook/src/lib/cssBrowserCompatibility.test.ts create mode 100644 packages/gitbook/src/lib/cssBrowserCompatibility.ts diff --git a/.github/workflows/css-browser-compatibility.yaml b/.github/workflows/css-browser-compatibility.yaml new file mode 100644 index 000000000..76be1dba0 --- /dev/null +++ b/.github/workflows/css-browser-compatibility.yaml @@ -0,0 +1,35 @@ +name: CSS browser compatibility + +on: + pull_request_target: + types: [opened, reopened, synchronize] + +permissions: + contents: read + issues: write + pull-requests: read + +concurrency: + group: css-browser-compatibility-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + check: + name: Check newly added CSS declarations + runs-on: ubuntu-latest + timeout-minutes: 6 + steps: + # pull_request_target checks out the trusted base branch by default. Never use the PR ref here. + - name: Checkout trusted checker + uses: actions/checkout@v4 + - name: Setup Bun + uses: ./.github/composite/setup-bun + - name: Install dependencies + run: bun install --frozen-lockfile + env: + PUPPETEER_SKIP_DOWNLOAD: 1 + - name: Check CSS browser compatibility + working-directory: packages/gitbook + run: bun run check:css-browser-compatibility + env: + GITHUB_TOKEN: ${{ github.token }} diff --git a/bun.lock b/bun.lock index ec0064604..74d33eb60 100644 --- a/bun.lock +++ b/bun.lock @@ -218,11 +218,13 @@ "@types/rison": "^0.0.9", "bun-types": "catalog:", "deepmerge": "^4.3.1", + "diff": "^8.0.2", "env-cmd": "^10.1.0", "jsonwebtoken": "^9.0.2", "postcss": "^8", "rss-parser": "^3.13.0", "stylelint": "^16.16.0", + "stylelint-no-unsupported-browser-features": "^8.1.1", "tailwindcss": "^4.1.11", "ts-essentials": "^10.0.1", "typescript": "catalog:", @@ -1700,6 +1702,8 @@ "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], + "@types/minimatch": ["@types/minimatch@3.0.5", "", {}, "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ=="], + "@types/ms": ["@types/ms@0.7.34", "", {}, "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="], "@types/negotiator": ["@types/negotiator@0.6.4", "", {}, "sha512-elf6BsTq+AkyNsb2h5cGNst2Mc7dPliVoAPm1fXglC/BM3f2pFA40BaSSv3E5lyHteEawVKLP+8TwiY1DMNb3A=="], @@ -1870,12 +1874,16 @@ "aria-hidden": ["aria-hidden@1.2.4", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A=="], + "array-differ": ["array-differ@3.0.0", "", {}, "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg=="], + "array-flatten": ["array-flatten@3.0.0", "", {}, "sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA=="], "array-timsort": ["array-timsort@1.0.3", "", {}, "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ=="], "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="], + "arrify": ["arrify@2.0.1", "", {}, "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug=="], + "assert-never": ["assert-never@1.4.0", "", {}, "sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA=="], "ast-kit": ["ast-kit@2.1.2", "", { "dependencies": { "@babel/parser": "^7.28.0", "pathe": "^2.0.3" } }, "sha512-cl76xfBQM6pztbrFWRnxbrDm9EOqDr1BF6+qQnnDZG2Co2LjyUktkN9GTJfBAfdae+DbT2nJf2nCGAdDDN7W2g=="], @@ -1926,10 +1934,12 @@ "bowser": ["bowser@2.11.0", "", {}, "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA=="], - "brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + "browserslist": ["browserslist@4.28.7", "", { "dependencies": { "baseline-browser-mapping": "^2.10.44", "caniuse-lite": "^1.0.30001806", "electron-to-chromium": "^1.5.393", "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw=="], + "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], @@ -2036,6 +2046,8 @@ "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], + "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], "cose-base": ["cose-base@1.0.3", "", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="], @@ -2050,6 +2062,8 @@ "css-select": ["css-select@5.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg=="], + "css-tokenize": ["css-tokenize@1.0.1", "", { "dependencies": { "inherits": "^2.0.1", "readable-stream": "^1.0.33" } }, "sha512-gLmmbJdwH9HLY4bcA17lnZ8GgPwEXRbvxBJGHnkiB6gLhRpTzjkjtMIvz7YORGW/Ptv2oMk8b5g+u7mRD6Dd7A=="], + "css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="], "css-what": ["css-what@6.1.0", "", {}, "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw=="], @@ -2184,6 +2198,8 @@ "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], + "doiuse": ["doiuse@6.0.6", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001760", "css-tokenize": "^1.0.1", "duplexify": "^4.1.3", "multimatch": "^5.0.0", "postcss": "^8.5.6", "source-map": "^0.7.6", "yargs": "^17.7.2" }, "bin": { "doiuse": "bin/cli.js" } }, "sha512-XuPRslcWHhQJ+WjCjimRUcNfhZvOiC0610FsY6WeSlzXvoZYtm6iOpR9K0N4wRoM/lP4i7LatT+IhltAzouSOw=="], + "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], @@ -2202,6 +2218,8 @@ "duplexer": ["duplexer@0.1.2", "", {}, "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="], + "duplexify": ["duplexify@4.1.3", "", { "dependencies": { "end-of-stream": "^1.4.1", "inherits": "^2.0.3", "readable-stream": "^3.1.1", "stream-shift": "^1.0.2" } }, "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA=="], + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], @@ -2212,6 +2230,8 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + "electron-to-chromium": ["electron-to-chromium@1.5.402", "", {}, "sha512-/oOpMaPT6Yg+6/1XQhyIPlzgj7Ye9zf+nNM2Uh6OcE2G2oNptWazFa+qB2Pdqqbsc9KnIDzgAntoYN0dbwOXwA=="], + "emoji-assets": ["emoji-assets@9.0.0", "", {}, "sha512-MkzxDIREHos8wcy5cCpSo58C4K/RM4HqYq2D1aBgT/G/7LQYvIq/L4Ye5eYMh1DWqXotlfTu0ENWrecpc1Hbkg=="], "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -2564,6 +2584,8 @@ "is-windows": ["is-windows@1.0.2", "", {}, "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="], + "isarray": ["isarray@0.0.1", "", {}, "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="], + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "jackspeak": ["jackspeak@4.1.1", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" } }, "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ=="], @@ -2858,6 +2880,8 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "multimatch": ["multimatch@5.0.0", "", { "dependencies": { "@types/minimatch": "^3.0.3", "array-differ": "^3.0.0", "array-union": "^2.1.0", "arrify": "^2.0.1", "minimatch": "^3.0.4" } }, "sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA=="], + "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], "nanoid": ["nanoid@5.1.5", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw=="], @@ -2878,6 +2902,8 @@ "node-gyp-build": ["node-gyp-build@4.8.2", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-IRUxE4BVsHWXkV/SFOut4qTlagw2aM8T5/vnTsmrHJvVoKueJHRc/JaFND7QDDc61kLYUJ6qlZM3sqTSyx2dTw=="], + "node-releases": ["node-releases@2.0.53", "", {}, "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ=="], + "nopt": ["nopt@8.1.0", "", { "dependencies": { "abbrev": "^3.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A=="], "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], @@ -3092,7 +3118,7 @@ "read-yaml-file": ["read-yaml-file@1.1.0", "", { "dependencies": { "graceful-fs": "^4.1.5", "js-yaml": "^3.6.1", "pify": "^4.0.1", "strip-bom": "^3.0.0" } }, "sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA=="], - "readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], + "readable-stream": ["readable-stream@1.1.14", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", "isarray": "0.0.1", "string_decoder": "~0.10.x" } }, "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ=="], "readdirp": ["readdirp@4.1.1", "", {}, "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw=="], @@ -3128,6 +3154,8 @@ "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], @@ -3242,6 +3270,8 @@ "statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], + "stream-shift": ["stream-shift@1.0.3", "", {}, "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ=="], + "stream-to-array": ["stream-to-array@2.3.0", "", { "dependencies": { "any-promise": "^1.1.0" } }, "sha512-UsZtOYEn4tWU2RGLOXr/o/xjRBftZRlG3dEWoaHr8j4GuypJ3isitGbVyjQKAuMu+xbiop8q224TjiZWc4XTZA=="], "stream-to-promise": ["stream-to-promise@2.2.0", "", { "dependencies": { "any-promise": "~1.3.0", "end-of-stream": "~1.1.0", "stream-to-array": "~2.3.0" } }, "sha512-HAGUASw8NT0k8JvIVutB2Y/9iBk7gpgEyAudXwNJmZERdMITGdajOa4VJfD/kNiA3TppQpTP4J+CtcHwdzKBAw=="], @@ -3250,7 +3280,7 @@ "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + "string_decoder": ["string_decoder@0.10.31", "", {}, "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ=="], "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], @@ -3272,6 +3302,8 @@ "stylelint": ["stylelint@16.16.0", "", { "dependencies": { "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "@csstools/media-query-list-parser": "^4.0.2", "@csstools/selector-specificity": "^5.0.0", "@dual-bundle/import-meta-resolve": "^4.1.0", "balanced-match": "^2.0.0", "colord": "^2.9.3", "cosmiconfig": "^9.0.0", "css-functions-list": "^3.2.3", "css-tree": "^3.1.0", "debug": "^4.3.7", "fast-glob": "^3.3.3", "fastest-levenshtein": "^1.0.16", "file-entry-cache": "^10.0.7", "global-modules": "^2.0.0", "globby": "^11.1.0", "globjoin": "^0.1.4", "html-tags": "^3.3.1", "ignore": "^7.0.3", "imurmurhash": "^0.1.4", "is-plain-object": "^5.0.0", "known-css-properties": "^0.35.0", "mathml-tag-names": "^2.1.3", "meow": "^13.2.0", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.5.3", "postcss-resolve-nested-selector": "^0.1.6", "postcss-safe-parser": "^7.0.1", "postcss-selector-parser": "^7.1.0", "postcss-value-parser": "^4.2.0", "resolve-from": "^5.0.0", "string-width": "^4.2.3", "supports-hyperlinks": "^3.2.0", "svg-tags": "^1.0.0", "table": "^6.9.0", "write-file-atomic": "^5.0.1" }, "bin": { "stylelint": "bin/stylelint.mjs" } }, "sha512-40X5UOb/0CEFnZVEHyN260HlSSUxPES+arrUphOumGWgXERHfwCD0kNBVILgQSij8iliYVwlc0V7M5bcLP9vPg=="], + "stylelint-no-unsupported-browser-features": ["stylelint-no-unsupported-browser-features@8.1.1", "", { "dependencies": { "browserslist": "^4.26.3", "doiuse": "^6.0.6", "postcss": "^8.4.32" }, "peerDependencies": { "stylelint": ">=16.0.2" } }, "sha512-sLEe6NUFoWL2vGt6IKKllQEKfKgVxmvTBFs1JVMHKKLWzxtWAXaqSxJvH+j0URM4vLQQqzC/wXc9Kp3XBNKdBw=="], + "stylis": ["stylis@4.3.6", "", {}, "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ=="], "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], @@ -3406,6 +3438,8 @@ "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + "update-browserslist-db": ["update-browserslist-db@1.3.0", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-x/M6q3w4Ybp91CNaS4S69UnliqR3BzRpOT6LWbksjth0S/+jhfaPJsWjt/TewpT8j9eLIojUf5jr29WextHroA=="], + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], "url-join": ["url-join@5.0.0", "", {}, "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA=="], @@ -4068,6 +4102,10 @@ "brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.11.12", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA=="], + + "browserslist/caniuse-lite": ["caniuse-lite@1.0.30001809", "", {}, "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ=="], + "bun-types/@types/node": ["@types/node@20.12.14", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg=="], "call-bound/get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], @@ -4106,10 +4144,20 @@ "d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="], + "doiuse/caniuse-lite": ["caniuse-lite@1.0.30001809", "", {}, "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ=="], + + "doiuse/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + + "doiuse/yargs": ["yargs@17.7.3", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g=="], + "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], "dunder-proto/gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + "duplexify/end-of-stream": ["end-of-stream@1.4.4", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q=="], + + "duplexify/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "edge-runtime/async-listen": ["async-listen@3.0.1", "", {}, "sha512-cWMaNwUJnf37C/S5TfCkk/15MwbPRwVYALA2jtjkbHjCmAPiDXyNJy2q3p1KAZzDLHAWyarUWSujUoHR4pEgrA=="], "edge-runtime/picocolors": ["picocolors@1.0.0", "", {}, "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="], @@ -4186,6 +4234,8 @@ "mlly/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "multimatch/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], "p-filter/p-map": ["p-map@2.1.0", "", {}, "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw=="], @@ -4194,6 +4244,8 @@ "path-scurry/lru-cache": ["lru-cache@11.0.2", "", {}, "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA=="], + "pino-abstract-transport/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], + "pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], @@ -4596,8 +4648,6 @@ "@tailwindcss/postcss/postcss/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="], - "@ts-morph/common/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], - "@types/jsonwebtoken/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="], "@types/swagger2openapi/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="], @@ -4716,6 +4766,12 @@ "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="], + "doiuse/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "doiuse/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "duplexify/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + "edge-runtime/pretty-ms/parse-ms": ["parse-ms@2.1.0", "", {}, "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA=="], "express/debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], @@ -4734,6 +4790,8 @@ "next/postcss/picocolors": ["picocolors@1.1.0", "", {}, "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw=="], + "pino-abstract-transport/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + "radix-vue/@floating-ui/vue/@floating-ui/utils": ["@floating-ui/utils@0.2.8", "", {}, "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig=="], "radix-vue/@vueuse/core/@types/web-bluetooth": ["@types/web-bluetooth@0.0.20", "", {}, "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow=="], @@ -4792,6 +4850,8 @@ "@headlessui/react/@floating-ui/react/@floating-ui/react-dom/@floating-ui/dom": ["@floating-ui/dom@1.7.5", "", { "dependencies": { "@floating-ui/core": "^1.7.4", "@floating-ui/utils": "^0.2.10" } }, "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg=="], + "@node-minify/core/glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + "@node-minify/core/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "@node-minify/core/glob/path-scurry/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], @@ -4844,8 +4904,6 @@ "@scalar/workspace-store/@scalar/openapi-upgrader/@scalar/openapi-types/zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="], - "@ts-morph/common/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "@vercel/backends/rolldown/@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], "@vercel/fun/semver/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], @@ -4858,6 +4916,8 @@ "@zenuml/core/tailwindcss/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "doiuse/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "micro/raw-body/http-errors/depd": ["depd@1.1.2", "", {}, "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ=="], "micro/raw-body/http-errors/setprototypeof": ["setprototypeof@1.1.1", "", {}, "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="], @@ -4866,6 +4926,8 @@ "micro/raw-body/http-errors/toidentifier": ["toidentifier@1.0.0", "", {}, "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="], + "rimraf/glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + "rimraf/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], @@ -4874,6 +4936,8 @@ "@headlessui/react/@floating-ui/react/@floating-ui/react-dom/@floating-ui/dom/@floating-ui/core": ["@floating-ui/core@1.7.4", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg=="], + "@node-minify/core/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "@opennextjs/aws/express/body-parser/raw-body/http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], "@opennextjs/aws/express/qs/side-channel/object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], @@ -4896,6 +4960,8 @@ "@zenuml/core/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "rimraf/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "@opennextjs/aws/express/body-parser/raw-body/http-errors/statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], "@opennextjs/aws/express/serve-static/send/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json index 8a4c28b6e..5e031d2d3 100644 --- a/packages/gitbook/package.json +++ b/packages/gitbook/package.json @@ -110,10 +110,12 @@ "@types/negotiator": "^0.6.4", "bun-types": "catalog:", "deepmerge": "^4.3.1", + "diff": "^8.0.2", "env-cmd": "^10.1.0", "jsonwebtoken": "^9.0.2", "postcss": "^8", "stylelint": "^16.16.0", + "stylelint-no-unsupported-browser-features": "^8.1.1", "tailwindcss": "^4.1.11", "ts-essentials": "^10.0.1", "typescript": "catalog:", @@ -127,6 +129,7 @@ "dev": "env-cmd --silent -f ../../.env.local next --webpack", "build": "next build --webpack", "build:local": "GITBOOK_URL=http://localhost:3000 next build --webpack", + "check:css-browser-compatibility": "bun scripts/check-css-browser-compatibility.ts", "start": "GITBOOK_URL=http://localhost:3000 next start", "build:cloudflare": "GITBOOK_RUNTIME=cloudflare opennextjs-cloudflare build", "dev:cloudflare": "wrangler dev --port 8771 --env preview", diff --git a/packages/gitbook/scripts/check-css-browser-compatibility.ts b/packages/gitbook/scripts/check-css-browser-compatibility.ts new file mode 100644 index 000000000..2714f8034 --- /dev/null +++ b/packages/gitbook/scripts/check-css-browser-compatibility.ts @@ -0,0 +1,245 @@ +import { readFile } from 'node:fs/promises'; +import { + type CompatibilityDiagnostic, + type IssueCommentClient, + formatCompatibilityComment, + getCompatibilityDiagnostics, + upsertCompatibilityComment, +} from '../src/lib/cssBrowserCompatibility'; + +interface PullRequestEvent { + pull_request: { + base: { sha: string }; + head: { sha: string }; + number: number; + }; +} + +interface PullRequestFile { + filename: string; + previous_filename?: string; + status: 'added' | 'copied' | 'modified' | 'removed' | 'renamed' | 'unchanged'; +} + +interface ContentResponse { + content: string; + encoding: string; + sha: string; +} + +interface GitBlobResponse { + content: string; + encoding: string; +} + +interface IssueComment { + body: string; + id: number; + user: { login: string } | null; +} + +class GitHubRequestError extends Error { + constructor( + readonly status: number, + message: string + ) { + super(message); + } +} + +class GitHubApi implements IssueCommentClient { + constructor( + private readonly repository: string, + private readonly token: string + ) {} + + private async request(path: string, init?: RequestInit): Promise { + const response = await fetch(`https://api.github.com${path}`, { + ...init, + headers: { + Accept: 'application/vnd.github+json', + Authorization: `Bearer ${this.token}`, + 'Content-Type': 'application/json', + 'X-GitHub-Api-Version': '2022-11-28', + ...init?.headers, + }, + }); + const responseText = await response.text(); + + if (!response.ok) { + throw new GitHubRequestError( + response.status, + `${init?.method ?? 'GET'} ${path} failed: ${responseText}` + ); + } + + return JSON.parse(responseText) as T; + } + + async listPullRequestFiles(pullRequestNumber: number): Promise { + const files: PullRequestFile[] = []; + + for (let page = 1; ; page += 1) { + const result = await this.request( + `/repos/${this.repository}/pulls/${pullRequestNumber}/files?per_page=100&page=${page}` + ); + files.push(...result); + if (result.length < 100) { + return files; + } + } + } + + private async getContent(path: string, ref: string): Promise { + const encodedPath = path.split('/').map(encodeURIComponent).join('/'); + const content = await this.request( + `/repos/${this.repository}/contents/${encodedPath}?ref=${encodeURIComponent(ref)}` + ); + const response = content.content + ? content + : await this.request( + `/repos/${this.repository}/git/blobs/${content.sha}` + ); + + if (response.encoding !== 'base64') { + throw new Error( + `Unsupported GitHub content encoding for ${path}: ${response.encoding}` + ); + } + + return Buffer.from(response.content.replaceAll('\n', ''), 'base64').toString('utf8'); + } + + async getFileAtRef(path: string, ref: string): Promise { + return this.getContent(path, ref); + } + + async listIssueComments(issueNumber: number): Promise { + const comments: IssueComment[] = []; + + for (let page = 1; ; page += 1) { + const result = await this.request( + `/repos/${this.repository}/issues/${issueNumber}/comments?per_page=100&page=${page}` + ); + comments.push(...result); + if (result.length < 100) { + return comments; + } + } + } + + async createIssueComment(issueNumber: number, body: string): Promise { + await this.request(`/repos/${this.repository}/issues/${issueNumber}/comments`, { + body: JSON.stringify({ body }), + method: 'POST', + }); + } + + async updateIssueComment(commentId: number, body: string): Promise { + await this.request(`/repos/${this.repository}/issues/comments/${commentId}`, { + body: JSON.stringify({ body }), + method: 'PATCH', + }); + } +} + +async function getBrowserslist(api: GitHubApi, headSha: string): Promise { + const packageJson = JSON.parse( + await api.getFileAtRef('packages/gitbook/package.json', headSha) + ) as { browserslist?: string[] }; + + if (!packageJson.browserslist?.length) { + throw new Error('packages/gitbook/package.json must define a Browserslist configuration.'); + } + + return packageJson.browserslist; +} + +async function getBaseContent( + api: GitHubApi, + file: PullRequestFile, + baseSha: string +): Promise { + if (file.status === 'added') { + return ''; + } + + try { + return await api.getFileAtRef(file.previous_filename ?? file.filename, baseSha); + } catch (error) { + if (error instanceof GitHubRequestError && error.status === 404) { + return ''; + } + throw error; + } +} + +async function run(): Promise { + const token = process.env.GITHUB_TOKEN; + const repository = process.env.GITHUB_REPOSITORY; + const eventPath = process.env.GITHUB_EVENT_PATH; + + if (!token || !repository || !eventPath) { + throw new Error('GITHUB_TOKEN, GITHUB_REPOSITORY, and GITHUB_EVENT_PATH are required.'); + } + + const event = JSON.parse(await readFile(eventPath, 'utf8')) as PullRequestEvent; + const pullRequest = event.pull_request; + if (!pullRequest) { + throw new Error('This script must run from a pull request event.'); + } + + const api = new GitHubApi(repository, token); + const browsers = await getBrowserslist(api, pullRequest.head.sha); + const files = (await api.listPullRequestFiles(pullRequest.number)).filter( + (file) => file.status !== 'removed' && file.filename.endsWith('.css') + ); + const diagnostics: CompatibilityDiagnostic[] = []; + + for (const file of files) { + const [base, head] = await Promise.all([ + getBaseContent(api, file, pullRequest.base.sha), + api.getFileAtRef(file.filename, pullRequest.head.sha), + ]); + diagnostics.push( + ...(await getCompatibilityDiagnostics({ + base, + browsers, + file: file.filename, + head, + })) + ); + } + + const comment = formatCompatibilityComment({ + diagnostics, + headSha: pullRequest.head.sha, + repository, + }); + await upsertCompatibilityComment({ + body: comment, + createIfMissing: diagnostics.length > 0, + client: api, + issueNumber: pullRequest.number, + }); + + if (diagnostics.length === 0) { + console.log('CSS browser compatibility check passed.'); + return true; + } + + console.error('Unsupported CSS declarations found:'); + for (const diagnostic of diagnostics) { + console.error( + `${diagnostic.file}:${diagnostic.line} ${diagnostic.property} — ${diagnostic.unsupportedBrowsers}` + ); + } + return false; +} + +try { + process.exitCode = (await run()) ? 0 : 1; +} catch (error) { + console.error(error); + process.exitCode = 1; +} diff --git a/packages/gitbook/src/lib/cssBrowserCompatibility.test.ts b/packages/gitbook/src/lib/cssBrowserCompatibility.test.ts new file mode 100644 index 000000000..ed351e6f7 --- /dev/null +++ b/packages/gitbook/src/lib/cssBrowserCompatibility.test.ts @@ -0,0 +1,210 @@ +import { describe, expect, it } from 'bun:test'; +import { + COMMENT_MARKER, + type IssueCommentClient, + formatCompatibilityComment, + getAddedDeclarations, + getChangedLines, + getCompatibilityDiagnostics, + upsertCompatibilityComment, +} from './cssBrowserCompatibility'; + +const baseCSS = `.card { + color: red; +} +`; + +describe('getChangedLines', () => { + it('tracks added and removed line numbers independently', () => { + const changedLines = getChangedLines('one\ntwo\n', 'one\nthree\n'); + + expect(changedLines.added).toEqual(new Set([2])); + expect(changedLines.removed).toEqual(new Set([2])); + }); +}); + +describe('getAddedDeclarations', () => { + it('returns newly added declarations', () => { + const declarations = getAddedDeclarations( + baseCSS, + `.card { + color: red; + container-type: inline-size; +} +` + ); + + expect(declarations).toEqual([{ column: 5, line: 3, property: 'container-type' }]); + }); + + it('does not treat a value change as a newly added property', () => { + const declarations = getAddedDeclarations( + baseCSS, + `.card { + color: blue; +} +` + ); + + expect(declarations).toEqual([]); + }); + + it('uses the declaration start for multiline declarations', () => { + const declarations = getAddedDeclarations( + baseCSS, + `.card { + color: red; + background-image: linear-gradient( + red, + blue + ); +} +` + ); + + expect(declarations).toEqual([{ column: 5, line: 3, property: 'background-image' }]); + }); +}); + +describe('getCompatibilityDiagnostics', () => { + it('ignores compatible and custom properties', async () => { + const diagnostics = await getCompatibilityDiagnostics({ + base: baseCSS, + browsers: ['safari 12'], + file: 'packages/gitbook/src/example.css', + head: `.card { + color: red; + --card-color: red; + display: flex; +} +`, + }); + + expect(diagnostics).toEqual([]); + }); + + it('reports a newly added unsupported property once', async () => { + const diagnostics = await getCompatibilityDiagnostics({ + base: baseCSS, + browsers: ['safari 12'], + file: 'packages/gitbook/src/example.css', + head: `.card { + color: red; + appearance: none; +} +`, + }); + + expect(diagnostics).toHaveLength(1); + expect(diagnostics[0]).toMatchObject({ + file: 'packages/gitbook/src/example.css', + line: 3, + property: 'appearance', + unsupportedBrowsers: 'Safari 12', + }); + }); + + it('attributes same-line declarations to the property that uses the feature', async () => { + const diagnostics = await getCompatibilityDiagnostics({ + base: '.card { color: red; }\n', + browsers: ['safari 12'], + file: 'packages/gitbook/src/example.css', + head: '.card { color: red; appearance: none; }\n', + }); + + expect(diagnostics).toHaveLength(1); + expect(diagnostics[0]?.property).toBe('appearance'); + }); +}); + +class FakeCommentClient implements IssueCommentClient { + created: string[] = []; + comments: Array<{ + body: string; + id: number; + user: { login: string } | null; + }> = []; + updated: Array<{ body: string; id: number }> = []; + + async createIssueComment(_issueNumber: number, body: string): Promise { + this.created.push(body); + } + + async listIssueComments(): Promise { + return this.comments; + } + + async updateIssueComment(commentId: number, body: string): Promise { + this.updated.push({ body, id: commentId }); + } +} + +describe('PR comments', () => { + const diagnostics = [ + { + column: 5, + feature: 'css-container-queries', + file: 'packages/gitbook/src/example.css', + line: 3, + property: 'container-type', + unsupportedBrowsers: 'Safari 12', + }, + ]; + + it('formats an actionable compatibility report', () => { + const comment = formatCompatibilityComment({ + diagnostics, + headSha: 'abc123', + repository: 'GitbookIO/gitbook', + }); + + expect(comment).toContain(COMMENT_MARKER); + expect(comment).toContain('container-type'); + expect(comment).toContain('Safari 12'); + expect(comment).toContain('example.css:3'); + }); + + it('creates a failure comment, updates it on rerun, and marks it clean', async () => { + const client = new FakeCommentClient(); + const failureComment = formatCompatibilityComment({ + diagnostics, + headSha: 'abc123', + repository: 'GitbookIO/gitbook', + }); + + await upsertCompatibilityComment({ body: failureComment, client, issueNumber: 42 }); + expect(client.created).toEqual([failureComment]); + + client.comments = [{ body: failureComment, id: 9, user: { login: 'github-actions[bot]' } }]; + const passingComment = formatCompatibilityComment({ + diagnostics: [], + headSha: 'def456', + repository: 'GitbookIO/gitbook', + }); + await upsertCompatibilityComment({ + body: passingComment, + createIfMissing: false, + client, + issueNumber: 42, + }); + + expect(client.updated).toEqual([{ body: passingComment, id: 9 }]); + }); + + it('does not add a passing comment when no compatibility comment exists', async () => { + const client = new FakeCommentClient(); + await upsertCompatibilityComment({ + body: formatCompatibilityComment({ + diagnostics: [], + headSha: 'abc123', + repository: 'GitbookIO/gitbook', + }), + createIfMissing: false, + client, + issueNumber: 42, + }); + + expect(client.created).toEqual([]); + expect(client.updated).toEqual([]); + }); +}); diff --git a/packages/gitbook/src/lib/cssBrowserCompatibility.ts b/packages/gitbook/src/lib/cssBrowserCompatibility.ts new file mode 100644 index 000000000..13973f91c --- /dev/null +++ b/packages/gitbook/src/lib/cssBrowserCompatibility.ts @@ -0,0 +1,274 @@ +import { diffLines } from 'diff'; +import postcss, { type Declaration } from 'postcss'; +import stylelint from 'stylelint'; + +export const COMMENT_MARKER = ''; +const COMPATIBILITY_RULE = 'plugin/no-unsupported-browser-features'; +const MAX_COMMENT_DIAGNOSTICS = 50; + +export interface ChangedLines { + added: Set; + removed: Set; +} + +export interface AddedDeclaration { + column: number; + line: number; + property: string; +} + +export interface CompatibilityDiagnostic extends AddedDeclaration { + feature: string; + file: string; + unsupportedBrowsers: string; +} + +interface Comment { + body: string; + id: number; + user: { login: string } | null; +} + +export interface IssueCommentClient { + createIssueComment(issueNumber: number, body: string): Promise; + listIssueComments(issueNumber: number): Promise; + updateIssueComment(commentId: number, body: string): Promise; +} + +function lineCount(value: string): number { + return value === '' ? 0 : value.split('\n').length - (value.endsWith('\n') ? 1 : 0); +} + +/** Return the line numbers that differ in each version of a text file. */ +export function getChangedLines(base: string, head: string): ChangedLines { + const added = new Set(); + const removed = new Set(); + let baseLine = 1; + let headLine = 1; + + for (const change of diffLines(base, head)) { + const count = change.count ?? lineCount(change.value); + + if (change.added) { + for (let line = headLine; line < headLine + count; line += 1) { + added.add(line); + } + headLine += count; + } else if (change.removed) { + for (let line = baseLine; line < baseLine + count; line += 1) { + removed.add(line); + } + baseLine += count; + } else { + baseLine += count; + headLine += count; + } + } + + return { added, removed }; +} + +function declarationContext(declaration: Declaration): string { + const ancestors: string[] = []; + let node = declaration.parent; + + while (node && node.type !== 'root') { + if (node.type === 'rule') { + ancestors.push(`rule:${node.selector}`); + } else if (node.type === 'atrule') { + ancestors.push(`at:${node.name} ${node.params}`); + } + node = node.parent; + } + + return ancestors.reverse().join(' > '); +} + +function declarationKey(declaration: Declaration): string { + return `${declarationContext(declaration)}\u0000${declaration.prop}`; +} + +function declarationsAtLines(css: string, lines: Set): Declaration[] { + const root = postcss.parse(css); + const declarations: Declaration[] = []; + + root.walkDecls((declaration) => { + if (declaration.source?.start?.line && lines.has(declaration.source.start.line)) { + declarations.push(declaration); + } + }); + + return declarations; +} + +/** + * Ignore a declaration that merely changed an existing property in the same rule. + * A new duplicate declaration remains reportable because it has no matching removed line. + */ +export function getAddedDeclarations(base: string, head: string): AddedDeclaration[] { + const changedLines = getChangedLines(base, head); + const removedByKey = new Map(); + + for (const declaration of declarationsAtLines(base, changedLines.removed)) { + const key = declarationKey(declaration); + removedByKey.set(key, (removedByKey.get(key) ?? 0) + 1); + } + + return declarationsAtLines(head, changedLines.added).flatMap((declaration) => { + const key = declarationKey(declaration); + const removedCount = removedByKey.get(key) ?? 0; + + if (removedCount > 0) { + removedByKey.set(key, removedCount - 1); + return []; + } + + const line = declaration.source?.start?.line; + const column = declaration.source?.start?.column; + return line && column ? [{ column, line, property: declaration.prop }] : []; + }); +} + +function featureFromWarning(warning: string): string { + const feature = warning.match(/browser feature "([^"]+)"/i)?.[1]; + return feature ?? 'CSS feature'; +} + +function browsersFromWarning(warning: string): string { + const unsupported = warning.match( + /(?:not supported by|only partially supported by)\s+(.+?)\s+\(plugin\/no-unsupported-browser-features\)$/i + )?.[1]; + return unsupported ?? warning; +} + +export async function getCompatibilityDiagnostics({ + base, + browsers, + file, + head, +}: { + base: string; + browsers: string[]; + file: string; + head: string; +}): Promise { + const declarations = getAddedDeclarations(base, head); + if (declarations.length === 0) { + return []; + } + + const declarationsByLine = new Map(); + for (const declaration of declarations) { + declarationsByLine.set(declaration.line, [ + ...(declarationsByLine.get(declaration.line) ?? []), + declaration, + ]); + } + + const result = await stylelint.lint({ + code: head, + codeFilename: file, + config: { + plugins: ['stylelint-no-unsupported-browser-features'], + rules: { + [COMPATIBILITY_RULE]: [true, { browsers, ignorePartialSupport: false }], + }, + }, + }); + + const diagnostics = result.results.flatMap((lintResult) => + lintResult.warnings.flatMap((warning) => { + if (warning.rule !== COMPATIBILITY_RULE || !warning.line) { + return []; + } + + const declarationsOnLine = declarationsByLine.get(warning.line) ?? []; + const declarationsForWarning = warning.column + ? declarationsOnLine.filter((declaration) => declaration.column === warning.column) + : declarationsOnLine; + + return declarationsForWarning.map((declaration) => ({ + ...declaration, + feature: featureFromWarning(warning.text), + file, + unsupportedBrowsers: browsersFromWarning(warning.text), + })); + }) + ); + + return Array.from( + new Map( + diagnostics.map((diagnostic) => [ + `${diagnostic.file}:${diagnostic.line}:${diagnostic.property}:${diagnostic.feature}`, + diagnostic, + ]) + ).values() + ); +} + +function escapeTableCell(value: string): string { + return value.replaceAll('\\', '\\\\').replaceAll('|', '\\|').replaceAll('\n', ' '); +} + +function blobUrl(repository: string, headSha: string, file: string, line: number): string { + const encodedFile = file.split('/').map(encodeURIComponent).join('/'); + return `https://github.com/${repository}/blob/${headSha}/${encodedFile}#L${line}`; +} + +export function formatCompatibilityComment({ + diagnostics, + headSha, + repository, +}: { + diagnostics: CompatibilityDiagnostic[]; + headSha: string; + repository: string; +}): string { + if (diagnostics.length === 0) { + return `${COMMENT_MARKER}\n## CSS browser compatibility\n\nāœ… No newly added CSS declarations have browser-compatibility issues.`; + } + + const visibleDiagnostics = diagnostics.slice(0, MAX_COMMENT_DIAGNOSTICS); + const rows = visibleDiagnostics.map((diagnostic) => { + const location = `[${escapeTableCell(diagnostic.file)}:${diagnostic.line}](${blobUrl(repository, headSha, diagnostic.file, diagnostic.line)})`; + return `| ${location} | \`${escapeTableCell(diagnostic.property)}\` | ${escapeTableCell(diagnostic.unsupportedBrowsers)} |`; + }); + const remaining = diagnostics.length - visibleDiagnostics.length; + const heading = diagnostics.length === 1 ? 'declaration is' : 'declarations are'; + + return [ + COMMENT_MARKER, + '## CSS browser compatibility', + '', + `āŒ ${diagnostics.length} newly added CSS ${heading} not fully supported by the configured Browserslist targets.`, + '', + '| Location | Property | Unsupported browsers |', + '| --- | --- | --- |', + ...rows, + ...(remaining > 0 ? ['', `_${remaining} additional finding(s) omitted._`] : []), + ].join('\n'); +} + +export async function upsertCompatibilityComment({ + body, + createIfMissing = true, + client, + issueNumber, +}: { + body: string; + createIfMissing?: boolean; + client: IssueCommentClient; + issueNumber: number; +}): Promise { + const comments = await client.listIssueComments(issueNumber); + const existingComment = comments.find( + (comment) => + comment.user?.login === 'github-actions[bot]' && comment.body.includes(COMMENT_MARKER) + ); + + if (existingComment) { + await client.updateIssueComment(existingComment.id, body); + } else if (createIfMissing) { + await client.createIssueComment(issueNumber, body); + } +}