mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-10-27 15:37:05 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d47b7df4ae | |||
| 45b38a2225 | |||
| 1a9e8d635a |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix admin toolbar magnification, tooltip placement, title truncation and logo colour.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Mark blocks, pages and titles so comments can target them on change request previews.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix the double slash in the example page URLs of the MCP server's getPage and sendFeedback tools.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix blurry YouTube thumbnails in tabs by lazy-loading all embeds until they are visible.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Resolve the published content with the full URL in a single lookup and always send the visitor country.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Adjust tab panel and tab button padding so tabbed content has more breathing room.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Track `ask_trigger_click` insights event when an Ask entry point is clicked.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Read the production hostnames that receive the visitor country from the `GITBOOK_VISITOR_COUNTRY_HOSTNAMES` build-time env variable.
|
||||
@@ -70,6 +70,7 @@ runs:
|
||||
GITBOOK_BLOCK_SEARCH_INDEXATION: ${{ inputs.environment == 'preview' && 'true' || '' }}
|
||||
GITBOOK_ALLOW_CUSTOMIZATION_OVERRIDE: ${{ inputs.environment == 'preview' && 'true' || '' }}
|
||||
GITBOOK_DISABLE_INSIGHTS: ${{ inputs.environment == 'preview' && 'true' || '' }}
|
||||
GITBOOK_DISABLE_LOOKUP_ALTERNATIVES: ${{ inputs.environment == 'staging' && 'true' || '' }}
|
||||
shell: bash
|
||||
|
||||
- name: Upload the DO worker
|
||||
|
||||
@@ -78,6 +78,16 @@ runs:
|
||||
echo "GITBOOK_DISABLE_INSIGHTS=true" >> .vercel/.env.${{ inputs.environment }}.local
|
||||
echo "--- .vercel/.env.${{ inputs.environment }}.local after inject ---"
|
||||
cat .vercel/.env.${{ inputs.environment }}.local
|
||||
- name: Inject visitor country hostnames
|
||||
if: ${{ env.GITBOOK_VISITOR_COUNTRY_HOSTNAMES != '' }}
|
||||
shell: bash
|
||||
run: |
|
||||
echo "GITBOOK_VISITOR_COUNTRY_HOSTNAMES=$GITBOOK_VISITOR_COUNTRY_HOSTNAMES" >> .vercel/.env.${{ inputs.environment }}.local
|
||||
- name: Inject staging build env vars
|
||||
if: ${{ inputs.environment == 'staging' }}
|
||||
shell: bash
|
||||
run: |
|
||||
echo "GITBOOK_DISABLE_LOOKUP_ALTERNATIVES=true" >> .vercel/.env.${{ inputs.environment }}.local
|
||||
- name: Build Project Artifacts
|
||||
run: bun run vercel build --target=${{ inputs.environment }} --token=${{ inputs.vercelToken }}
|
||||
shell: bash
|
||||
|
||||
@@ -10,6 +10,8 @@ jobs:
|
||||
environment:
|
||||
name: 2v-production
|
||||
url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
env:
|
||||
GITBOOK_VISITOR_COUNTRY_HOSTNAMES: ${{ vars.GITBOOK_VISITOR_COUNTRY_HOSTNAMES }}
|
||||
outputs:
|
||||
deployment-url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
steps:
|
||||
@@ -31,6 +33,8 @@ jobs:
|
||||
environment:
|
||||
name: 2c-production
|
||||
url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
env:
|
||||
GITBOOK_VISITOR_COUNTRY_HOSTNAMES: ${{ vars.GITBOOK_VISITOR_COUNTRY_HOSTNAMES }}
|
||||
outputs:
|
||||
deployment-url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
steps:
|
||||
|
||||
@@ -129,8 +129,8 @@
|
||||
"@gitbook/react-openapi": "workspace:*",
|
||||
"@mermaid-js/mermaid-zenuml": "^0.2.2",
|
||||
"@modelcontextprotocol/sdk": "1.17.5",
|
||||
"@opennextjs/aws": "4.1.7",
|
||||
"@opennextjs/cloudflare": "1.20.8",
|
||||
"@opennextjs/aws": "4.1.11",
|
||||
"@opennextjs/cloudflare": "1.20.12",
|
||||
"@panzoom/panzoom": "^4.6.1",
|
||||
"@sindresorhus/fnv1a": "^3.1.0",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
@@ -160,7 +160,7 @@
|
||||
"micromark-extension-gfm": "^3.0.0",
|
||||
"motion": "^12.23.24",
|
||||
"negotiator": "^1.0.0",
|
||||
"next": "^16.3.8",
|
||||
"next": "^16.4.2",
|
||||
"next-themes": "^0.4.6",
|
||||
"nuqs": "^2.2.3",
|
||||
"object-hash": "^3.0.0",
|
||||
@@ -342,7 +342,6 @@
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"decode-named-character-reference@1.0.2": "patches/decode-named-character-reference@1.0.2.patch",
|
||||
"next@16.3.8": "patches/next@16.3.8.patch",
|
||||
"mermaid@11.16.1": "patches/mermaid@11.16.1.patch",
|
||||
},
|
||||
"overrides": {
|
||||
@@ -398,25 +397,25 @@
|
||||
|
||||
"@argos-ci/util": ["@argos-ci/util@4.0.0", "", {}, "sha512-2yrJr81OVpa2TkvSjzGZlGv6SVqZcxF62AVk6M79aZ7nXkwWFg3tjA8awbFwn2mODI67gkP5FarAD29bzx3P3w=="],
|
||||
|
||||
"@ast-grep/napi": ["@ast-grep/napi@0.40.5", "", { "optionalDependencies": { "@ast-grep/napi-darwin-arm64": "0.40.5", "@ast-grep/napi-darwin-x64": "0.40.5", "@ast-grep/napi-linux-arm64-gnu": "0.40.5", "@ast-grep/napi-linux-arm64-musl": "0.40.5", "@ast-grep/napi-linux-x64-gnu": "0.40.5", "@ast-grep/napi-linux-x64-musl": "0.40.5", "@ast-grep/napi-win32-arm64-msvc": "0.40.5", "@ast-grep/napi-win32-ia32-msvc": "0.40.5", "@ast-grep/napi-win32-x64-msvc": "0.40.5" } }, "sha512-hJA62OeBKUQT68DD2gDyhOqJxZxycqg8wLxbqjgqSzYttCMSDL9tiAQ9abgekBYNHudbJosm9sWOEbmCDfpX2A=="],
|
||||
"@ast-grep/napi": ["@ast-grep/napi@0.45.3", "", { "optionalDependencies": { "@ast-grep/napi-darwin-arm64": "0.45.3", "@ast-grep/napi-darwin-x64": "0.45.3", "@ast-grep/napi-linux-arm64-gnu": "0.45.3", "@ast-grep/napi-linux-arm64-musl": "0.45.3", "@ast-grep/napi-linux-x64-gnu": "0.45.3", "@ast-grep/napi-linux-x64-musl": "0.45.3", "@ast-grep/napi-win32-arm64-msvc": "0.45.3", "@ast-grep/napi-win32-ia32-msvc": "0.45.3", "@ast-grep/napi-win32-x64-msvc": "0.45.3" } }, "sha512-bjJsUt0g3UCwbn9bjv7MnoR15rszSm79yPCXn4DWWK0NuYUUqBU/srtNMR4WnpfR7TnfBRdYFoqSNx0pGCNWJg=="],
|
||||
|
||||
"@ast-grep/napi-darwin-arm64": ["@ast-grep/napi-darwin-arm64@0.40.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-2F072fGN0WTq7KI3okuEnkGJVEHLbi56Bw1H6NAMf7j2mJJeQWsRyGOMcyNnUXZDeNdvoMH0OB2a5wwUegY/nQ=="],
|
||||
"@ast-grep/napi-darwin-arm64": ["@ast-grep/napi-darwin-arm64@0.45.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rMZJ6frSpR/xSbeV4hiA9WR5hHqk3rVYX8lOqEoUCaUnjEuFXvHdGN4WawJ8pVDq/IqM1UnTtsDSYgUdVmO5DA=="],
|
||||
|
||||
"@ast-grep/napi-darwin-x64": ["@ast-grep/napi-darwin-x64@0.40.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-dJMidHZhhxuLBYNi6/FKI812jQ7wcFPSKkVPwviez2D+KvYagapUMAV/4dJ7FCORfguVk8Y0jpPAlYmWRT5nvA=="],
|
||||
"@ast-grep/napi-darwin-x64": ["@ast-grep/napi-darwin-x64@0.45.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-tIclZ5pM+YSgk8NNTBoTkaclbS39cU5lbVfSrFWX4LUnxXfUPG+F+ppxyFjKwJTBELsGHEO4LdX3/Lc+FDwPNA=="],
|
||||
|
||||
"@ast-grep/napi-linux-arm64-gnu": ["@ast-grep/napi-linux-arm64-gnu@0.40.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-nBRCbyoS87uqkaw4Oyfe5VO+SRm2B+0g0T8ME69Qry9ShMf41a2bTdpcQx9e8scZPogq+CTwDHo3THyBV71l9w=="],
|
||||
"@ast-grep/napi-linux-arm64-gnu": ["@ast-grep/napi-linux-arm64-gnu@0.45.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Bm8F7/oRHQkDXbjH+gBi9oB5huJ0q12r/AIOMIypf5PWlAVvh206Tsw468phw4Oh6kd4Ad1Z+gflNW1MutgK2g=="],
|
||||
|
||||
"@ast-grep/napi-linux-arm64-musl": ["@ast-grep/napi-linux-arm64-musl@0.40.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-/qKsmds5FMoaEj6FdNzepbmLMtlFuBLdrAn9GIWCqOIcVcYvM1Nka8+mncfeXB/MFZKOrzQsQdPTWqrrQzXLrA=="],
|
||||
"@ast-grep/napi-linux-arm64-musl": ["@ast-grep/napi-linux-arm64-musl@0.45.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-yr5xcRerV41GeRxmZ8SKbfGeyZRhQePoNNCgTAb4OrWMelWwbMI80aY7/IlF2bo1sN40BSW4hZdGIO/akTd1RQ=="],
|
||||
|
||||
"@ast-grep/napi-linux-x64-gnu": ["@ast-grep/napi-linux-x64-gnu@0.40.5", "", { "os": "linux", "cpu": "x64" }, "sha512-DP4oDbq7f/1A2hRTFLhJfDFR6aI5mRWdEfKfHzRItmlKsR9WlcEl1qDJs/zX9R2EEtIDsSKRzuJNfJllY3/W8Q=="],
|
||||
"@ast-grep/napi-linux-x64-gnu": ["@ast-grep/napi-linux-x64-gnu@0.45.3", "", { "os": "linux", "cpu": "x64" }, "sha512-lluRK3hxfHMQdFuVID6seAKVx4FH0kEwF6rCh5/I40Sf3EtNR2UCBXAqd6RBA6hqlSckktvBxCfw+h4aescDmg=="],
|
||||
|
||||
"@ast-grep/napi-linux-x64-musl": ["@ast-grep/napi-linux-x64-musl@0.40.5", "", { "os": "linux", "cpu": "x64" }, "sha512-BRZUvVBPUNpWPo6Ns8chXVzxHPY+k9gpsubGTHy92Q26ecZULd/dTkWWdnvfhRqttsSQ9Pe/XQdi5+hDQ6RYcg=="],
|
||||
"@ast-grep/napi-linux-x64-musl": ["@ast-grep/napi-linux-x64-musl@0.45.3", "", { "os": "linux", "cpu": "x64" }, "sha512-09wJQKq86LqILqQtw0+X09SzhcPwUnMVFKyia7S3XLbLqSeQpAMF55Y+zll53JRQ1dCzFEZJyvTT/7j9vwfE3w=="],
|
||||
|
||||
"@ast-grep/napi-win32-arm64-msvc": ["@ast-grep/napi-win32-arm64-msvc@0.40.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-y95zSEwc7vhxmcrcH0GnK4ZHEBQrmrszRBNQovzaciF9GUqEcCACNLoBesn4V47IaOp4fYgD2/EhGRTIBFb2Ug=="],
|
||||
"@ast-grep/napi-win32-arm64-msvc": ["@ast-grep/napi-win32-arm64-msvc@0.45.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-/HiVgw2cdtzXLDLoG4eBBcauekNXewfagy7qiVbjkVu/KcCH8dJ9lpj8DzeWwuBNtZxuD8jpiwP0M1pz3Bc3uw=="],
|
||||
|
||||
"@ast-grep/napi-win32-ia32-msvc": ["@ast-grep/napi-win32-ia32-msvc@0.40.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-K/u8De62iUnFCzVUs7FBdTZ2Jrgc5/DLHqjpup66KxZ7GIM9/HGME/O8aSoPkpcAeCD4TiTZ11C1i5p5H98hTg=="],
|
||||
"@ast-grep/napi-win32-ia32-msvc": ["@ast-grep/napi-win32-ia32-msvc@0.45.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-/gwJrJQUw65uNtAFcG2rtSTr117Gqqy3bfjaljCPKsa65dTeRcMMnjKLlZ6V9/g8z+4Xf78kdhp5ECbir4yfSw=="],
|
||||
|
||||
"@ast-grep/napi-win32-x64-msvc": ["@ast-grep/napi-win32-x64-msvc@0.40.5", "", { "os": "win32", "cpu": "x64" }, "sha512-dqm5zg/o4Nh4VOQPEpMS23ot8HVd22gG0eg01t4CFcZeuzyuSgBlOL3N7xLbz3iH2sVkk7keuBwAzOIpTqziNQ=="],
|
||||
"@ast-grep/napi-win32-x64-msvc": ["@ast-grep/napi-win32-x64-msvc@0.45.3", "", { "os": "win32", "cpu": "x64" }, "sha512-CEZAMAsaDoQoVE3MT2c/ry3/ydfyaqlTjxv8DK5PG355SrdAYPgevGC2Me6tz4dhphJin89Y20VsnrS5UrYV5Q=="],
|
||||
|
||||
"@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="],
|
||||
|
||||
@@ -626,12 +625,16 @@
|
||||
|
||||
"@csstools/selector-specificity": ["@csstools/selector-specificity@5.0.0", "", { "peerDependencies": { "postcss-selector-parser": "^7.0.0" } }, "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw=="],
|
||||
|
||||
"@dotenvx/dotenvx": ["@dotenvx/dotenvx@1.31.0", "", { "dependencies": { "commander": "^11.1.0", "dotenv": "^16.4.5", "eciesjs": "^0.4.10", "execa": "^5.1.1", "fdir": "^6.2.0", "ignore": "^5.3.0", "object-treeify": "1.1.33", "picomatch": "^4.0.2", "which": "^4.0.0" }, "bin": { "dotenvx": "src/cli/dotenvx.js", "git-dotenvx": "src/cli/dotenvx.js" } }, "sha512-GeDxvtjiRuoyWVU9nQneId879zIyNdL05bS7RKiqMkfBSKpHMWHLoRyRqjYWLaXmX/llKO1hTlqHDmatkQAjPA=="],
|
||||
"@dotenvx/dotenvx": ["@dotenvx/dotenvx@2.38.1", "", { "dependencies": { "@dotenvx/primitives": "^3.3.1", "@dotenvx/providers": "^0.3.11", "@dotenvx/tooling": "^1.0.5", "undici": "^6.28.1", "write-file-atomic": "^5.0.1", "yocto-spinner": "^1.2.1" }, "bin": { "dx": "src/cli/dotenvx.js", "dotenvx": "src/cli/dotenvx.js" } }, "sha512-/E1w1e5+XjX13+BsGAupICHNwGVbgcsSQ/+2bAUXWlovNAt567SNqfON8ATkF1jUD+5OuxOhez2dt+IBifHG8A=="],
|
||||
|
||||
"@dotenvx/primitives": ["@dotenvx/primitives@3.3.1", "", {}, "sha512-ykX29rVnHwwV6m3AMmvR+niFSwhOG2TYttJjURz3n/3iHtpWC8gEptojYY+eHgBAV4fkaGRM1Rf0wgQ6/DeUYQ=="],
|
||||
|
||||
"@dotenvx/providers": ["@dotenvx/providers@0.3.11", "", { "dependencies": { "@dotenvx/primitives": "^3.3.1", "@dotenvx/tooling": "^1.0.4" } }, "sha512-5H5kAr5ps4shR7ZtA3mtDphh9Fbhp21yY77jCBCJLX2pbdEse0bcCBn7M1Ex1YrKF2O9SLKJB8ttJ2P5JsPidQ=="],
|
||||
|
||||
"@dotenvx/tooling": ["@dotenvx/tooling@1.0.5", "", {}, "sha512-XMAwZpxIajWaP31zLl2GQS3grIQsRY4B4VMss8fDv70XbGF/9aXSMHc3v9iJNZZVX/i0Loz1MAn6oM8dycbN4A=="],
|
||||
|
||||
"@dual-bundle/import-meta-resolve": ["@dual-bundle/import-meta-resolve@4.1.0", "", {}, "sha512-+nxncfwHM5SgAtrVzgpzJOI1ol0PkumhVo469KCf9lUi21IGcY90G98VuHm9VRrUypmAzawAHO9bs6hqeADaVg=="],
|
||||
|
||||
"@ecies/ciphers": ["@ecies/ciphers@0.2.2", "", { "peerDependencies": { "@noble/ciphers": "^1.0.0" } }, "sha512-ylfGR7PyTd+Rm2PqQowG08BCKA22QuX8NzrL+LxAAvazN10DMwdJ2fWwAzRj05FI/M8vNFGm3cv9Wq/GFWCBLg=="],
|
||||
|
||||
"@edge-runtime/format": ["@edge-runtime/format@2.2.1", "", {}, "sha512-JQTRVuiusQLNNLe2W9tnzBlV/GvSVcozLl4XZHk5swnRZ/v6jp8TqR8P7sqmJsQqblDZ3EztcWmLDbhRje/+8g=="],
|
||||
|
||||
"@edge-runtime/node-utils": ["@edge-runtime/node-utils@2.3.0", "", {}, "sha512-uUtx8BFoO1hNxtHjp3eqVPC/mWImGb2exOfGjMLUoipuWgjej+f4o/VP4bUI8U40gu7Teogd5VTeZUkGvJSPOQ=="],
|
||||
@@ -772,57 +775,57 @@
|
||||
|
||||
"@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="],
|
||||
|
||||
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.3" }, "os": "darwin", "cpu": "arm64" }, "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA=="],
|
||||
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg=="],
|
||||
|
||||
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.3" }, "os": "darwin", "cpu": "x64" }, "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw=="],
|
||||
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.4" }, "os": "darwin", "cpu": "x64" }, "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw=="],
|
||||
|
||||
"@img/sharp-freebsd-wasm32": ["@img/sharp-freebsd-wasm32@0.35.4", "", { "dependencies": { "@img/sharp-wasm32": "0.35.4" }, "os": "freebsd" }, "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA=="],
|
||||
"@img/sharp-freebsd-wasm32": ["@img/sharp-freebsd-wasm32@0.35.5", "", { "dependencies": { "@img/sharp-wasm32": "0.35.5" }, "os": "freebsd" }, "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg=="],
|
||||
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw=="],
|
||||
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.3", "", { "os": "linux", "cpu": "arm" }, "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA=="],
|
||||
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.4", "", { "os": "linux", "cpu": "arm" }, "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A=="],
|
||||
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A=="],
|
||||
|
||||
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w=="],
|
||||
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ=="],
|
||||
|
||||
"@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.3", "", { "os": "linux", "cpu": "none" }, "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ=="],
|
||||
"@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.4", "", { "os": "linux", "cpu": "none" }, "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q=="],
|
||||
|
||||
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q=="],
|
||||
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ=="],
|
||||
|
||||
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA=="],
|
||||
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.4", "", { "os": "linux", "cpu": "x64" }, "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw=="],
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw=="],
|
||||
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA=="],
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw=="],
|
||||
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.4", "", { "os": "linux", "cpu": "x64" }, "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg=="],
|
||||
|
||||
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.3" }, "os": "linux", "cpu": "arm" }, "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A=="],
|
||||
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.4" }, "os": "linux", "cpu": "arm" }, "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ=="],
|
||||
|
||||
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.3" }, "os": "linux", "cpu": "arm64" }, "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ=="],
|
||||
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.4" }, "os": "linux", "cpu": "arm64" }, "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ=="],
|
||||
|
||||
"@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.3" }, "os": "linux", "cpu": "ppc64" }, "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ=="],
|
||||
"@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA=="],
|
||||
|
||||
"@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.3" }, "os": "linux", "cpu": "none" }, "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA=="],
|
||||
"@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.4" }, "os": "linux", "cpu": "none" }, "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ=="],
|
||||
|
||||
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.3" }, "os": "linux", "cpu": "s390x" }, "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ=="],
|
||||
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.4" }, "os": "linux", "cpu": "s390x" }, "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA=="],
|
||||
|
||||
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.3" }, "os": "linux", "cpu": "x64" }, "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA=="],
|
||||
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.4" }, "os": "linux", "cpu": "x64" }, "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog=="],
|
||||
|
||||
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" }, "os": "linux", "cpu": "arm64" }, "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA=="],
|
||||
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" }, "os": "linux", "cpu": "arm64" }, "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg=="],
|
||||
|
||||
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.3" }, "os": "linux", "cpu": "x64" }, "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg=="],
|
||||
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.4" }, "os": "linux", "cpu": "x64" }, "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A=="],
|
||||
|
||||
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="],
|
||||
|
||||
"@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.4", "", { "dependencies": { "@img/sharp-wasm32": "0.35.4" }, "cpu": "none" }, "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA=="],
|
||||
"@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.5", "", { "dependencies": { "@img/sharp-wasm32": "0.35.5" }, "cpu": "none" }, "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA=="],
|
||||
|
||||
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.35.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw=="],
|
||||
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.35.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q=="],
|
||||
|
||||
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw=="],
|
||||
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A=="],
|
||||
|
||||
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.4", "", { "os": "win32", "cpu": "x64" }, "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg=="],
|
||||
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.5", "", { "os": "win32", "cpu": "x64" }, "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg=="],
|
||||
|
||||
"@inquirer/external-editor": ["@inquirer/external-editor@1.0.2", "", { "dependencies": { "chardet": "^2.1.0", "iconv-lite": "^0.7.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-yy9cOoBnx58TlsPrIxauKIFQTiyH+0MK4e97y4sV9ERbI+zDxw7i2hxHLCIEGIE/8PPvDxGhgzIOTSOWcs6/MQ=="],
|
||||
|
||||
@@ -884,35 +887,29 @@
|
||||
|
||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.6", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" } }, "sha512-DXj75ewm11LIWUk198QSKUTxjyRjsBwk09MuMk5DGK+GDUtyPhhEHOGP/Xwwj3DjQXXkivoBirmOnKrLfc0+9g=="],
|
||||
|
||||
"@next/env": ["@next/env@16.3.8", "", {}, "sha512-Al9zqHVV7TJv0eFuOU4U7Lvv74PTih4Ch63sk2xCIpSTkE3udFnaOcnzP2lQVymiL7yS9Cj2iClUXlR3EQ5sEw=="],
|
||||
"@next/env": ["@next/env@16.4.2", "", {}, "sha512-nTUTFH7G5/J/fmpkjVLegOkqyKOP0CEsJcMsyO4UCYeN1su+YMW/MfaSErnJskL7XM+UDK6aTvslxsRCyya/sA=="],
|
||||
|
||||
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.3.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-2JPRMh2nmQG5CiL7cXGL9AGwnPWJQ//cTtAUCT+w511QHk79SYz3LGv/pc5X643B/WEO0rvu3Yww0hqwt3kgeA=="],
|
||||
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.4.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ONkhFvZNFWlsEHOM/81AtxdyuJP2KRYKYR0JxPwvcu1AuPMMMgcL+wW2luldJvMtZwKtUxoSkCychW5wIeoYSw=="],
|
||||
|
||||
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.3.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-GZtCCOBKJ4leVIT/Th0llWKhD1ca92lzbQiS5R5ON9QkoiFnilFsebDae1JU2a3HWoKMEmEZWGs1AGLavVM72Q=="],
|
||||
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.4.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-luPCMbOsCoaF+kXJEUjmjmOZg1lC3BflhmUu+ZmW4tBu2vW/4AXak8JBFvePizGkxVmNiAw/r9ra+6UviCWtWQ=="],
|
||||
|
||||
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.3.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-O659ygeQYqneJ1fBKMpFxIFqYkYswu8IAS1OCKK/4f3ZgJJm1dRz4fVJZRi/kLLWjnBKnebOePA4WNv+sV1pVA=="],
|
||||
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.4.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-xhwFTAncJyaCNy5KqKKuKaIIdCy1/1ZV1s5S1G0hhT7wGMkDTlfJ6aQbfRFnIG/s3/kDrxQ8aycVe+c5QnP8ow=="],
|
||||
|
||||
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.3.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-dSjKSyWpzxoO1d3DIZZcP4XJcNaKeLmxQMFOiYl5vuBRMmweIqnAhty8tAmRsvTss779cK1FtYnDMj40e4TQlg=="],
|
||||
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.4.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-T1PNmfbPcxr5ndtWUL/3wQtLXsz808lq6sc2GrNT5k2oquF7UyqZddHSuAewHVsWsGXe6nsl9HoN1YNriVjuSg=="],
|
||||
|
||||
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.3.8", "", { "os": "linux", "cpu": "x64" }, "sha512-lbqOuz3RPRcv+o9msNsJw5x4+Y1ZwPTs6vmL6DCf7i0fZfvng/F59wyeDwqHIvV0mK//RBy/jJkZ+nCKsSMXjQ=="],
|
||||
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.4.2", "", { "os": "linux", "cpu": "x64" }, "sha512-N9cDUOCGtkJG5PNKDFZUF3xXndrJJcpLp121uAGjIGvO/U2p9tfdNOM9htj7+5f72l0m8rMw/cvYacUJBQoudg=="],
|
||||
|
||||
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.3.8", "", { "os": "linux", "cpu": "x64" }, "sha512-+316WswI8ScVgZeUd+1KGaXkHhaYQzCjvH/05TZSpJ8zBizb1a4G7DtO7F12jcBIqMOtsz9ji1t48fmKtzqsGA=="],
|
||||
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.4.2", "", { "os": "linux", "cpu": "x64" }, "sha512-yY2uh1YhX6ufuYy2FCbw1dqblbhI57GLYAJfCkqD9auTv6pLrH67Uccjc3QMKBeCYAG6lkgu08ExmX1bbjejjQ=="],
|
||||
|
||||
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.3.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-ji0gd4kMYUxO+1fJBIbiBVRCjzG/lloiyCccnlebvb1ZJ5qXCPZqYg4Jl1DrrixnWNMKylzgpmMWx0yNDYXlzw=="],
|
||||
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.4.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-j/ArWRIHpMMB6wYLMhr2puTguBt70lNU/Cmjl9p6dTDE8DGknYJMP9PdwPk2dqpCNL/KIX9yfngDvIesxJPmsg=="],
|
||||
|
||||
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.3.8", "", { "os": "win32", "cpu": "x64" }, "sha512-WcTlaKt/TWkh5kUjdJcUmB1XgZ+1c6fz4Y9fDHL73YNSdGaUWjceeWrrlwF0nv19iABYWC4iAq1oX1w4Bn0vfg=="],
|
||||
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.4.2", "", { "os": "win32", "cpu": "x64" }, "sha512-AA4edyWHDko3NCSo0x6GomsdgFb0oR1J3MXU1b5xt6FRFg4AucrMPW3S0hCe/9onU0O1BbpGQBsqo1r0Gneu1g=="],
|
||||
|
||||
"@noble/ciphers": ["@noble/ciphers@1.2.1", "", {}, "sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA=="],
|
||||
"@node-minify/core": ["@node-minify/core@9.0.2", "", { "dependencies": { "@node-minify/utils": "9.0.1", "glob": "10.3.3", "mkdirp": "3.0.1" } }, "sha512-FNhv29Wom6wKrrFKaeAfmZqz7TX5A1E6P+bpd0VIc+DYWMLUIhAViS8riaZg3A1oD0s06s+5BG2Fg7RqMKiKHw=="],
|
||||
|
||||
"@noble/curves": ["@noble/curves@1.8.1", "", { "dependencies": { "@noble/hashes": "1.7.1" } }, "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ=="],
|
||||
"@node-minify/terser": ["@node-minify/terser@9.0.1", "", { "dependencies": { "@node-minify/utils": "9.0.1", "terser": "5.36.0" } }, "sha512-WF78Ex+/xNZZDYvzwB7+sLUYQbJzyyS36ZjMhVqhORejHOsoOcTjQ9TdOgKZoY7wlfxjnLxeMKkO8R/R1KL9aQ=="],
|
||||
|
||||
"@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="],
|
||||
|
||||
"@node-minify/core": ["@node-minify/core@8.0.6", "", { "dependencies": { "@node-minify/utils": "8.0.6", "glob": "9.3.5", "mkdirp": "1.0.4" } }, "sha512-/vxN46ieWDLU67CmgbArEvOb41zlYFOkOtr9QW9CnTrBLuTyGgkyNWC2y5+khvRw3Br58p2B5ZVSx/PxCTru6g=="],
|
||||
|
||||
"@node-minify/terser": ["@node-minify/terser@8.0.6", "", { "dependencies": { "@node-minify/utils": "8.0.6", "terser": "5.16.9" } }, "sha512-grQ1ipham743ch2c3++C8Isk6toJnxJSyDiwUI/IWUCh4CZFD6aYVw6UAY40IpCnjrq5aXGwiv5OZJn6Pr0hvg=="],
|
||||
|
||||
"@node-minify/utils": ["@node-minify/utils@8.0.6", "", { "dependencies": { "gzip-size": "6.0.0" } }, "sha512-csY4qcR7jUwiZmkreNTJhcypQfts2aY2CK+a+rXgXUImZiZiySh0FvwHjRnlqWKvg+y6ae9lHFzDRjBTmqlTIQ=="],
|
||||
"@node-minify/utils": ["@node-minify/utils@9.0.1", "", { "dependencies": { "gzip-size": "6.0.0" } }, "sha512-aC1+mhKTP3IMa2VcuGl3ui92LO/7CPQWldNGzu3BVGKiMNJ70AKJW/R6huuYCSuQyHDGM9oFwiVClsZnFxn67g=="],
|
||||
|
||||
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
||||
|
||||
@@ -936,9 +933,9 @@
|
||||
|
||||
"@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="],
|
||||
|
||||
"@opennextjs/aws": ["@opennextjs/aws@4.1.7", "", { "dependencies": { "@ast-grep/napi": "^0.40.5", "@aws-sdk/client-cloudfront": "3.984.0", "@aws-sdk/client-dynamodb": "3.984.0", "@aws-sdk/client-lambda": "3.984.0", "@aws-sdk/client-s3": "3.984.0", "@aws-sdk/client-sqs": "3.984.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.3", "aws4fetch": "^1.0.20", "chalk": "^5.6.2", "cookie": "^1.0.2", "esbuild": "0.25.4", "express": "^5.1.0", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.1.0", "yaml": "^2.8.1" }, "peerDependencies": { "next": ">=15.5.27 <16 || >=16.3.8" }, "bin": { "open-next": "dist/index.js" } }, "sha512-ELezSARrTfxp/76cz41eQjki1AV0CuBUKOznU3w/tOk7i87x867RkkYho+gDkog9gpnGzefHdJu463o9VaabNQ=="],
|
||||
"@opennextjs/aws": ["@opennextjs/aws@4.1.11", "", { "dependencies": { "@ast-grep/napi": "^0.45.3", "@aws-sdk/client-cloudfront": "3.984.0", "@aws-sdk/client-dynamodb": "3.984.0", "@aws-sdk/client-lambda": "3.984.0", "@aws-sdk/client-s3": "3.984.0", "@aws-sdk/client-sqs": "3.984.0", "@node-minify/core": "^9.0.2", "@node-minify/terser": "^9.0.1", "@tsconfig/node18": "^18.2.7", "aws4fetch": "^1.0.20", "chalk": "^5.6.2", "cookie": "^1.1.1", "esbuild": "0.28.2", "express": "^5.2.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.1.0", "yaml": "^2.9.1" }, "peerDependencies": { "next": ">=15.5.28 <16 || >=16.4.1" }, "bin": { "open-next": "dist/index.js" } }, "sha512-cew0cUgGZF/Kr/C+Z1qvmAdKj4Va5cRho+aB6Ci/5ILd+QDdrR2LVaC/6Qm12KVF37vSLDK7lPDd/KdvvCoNEw=="],
|
||||
|
||||
"@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.20.8", "", { "dependencies": { "@ast-grep/napi": "^0.40.5", "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "4.1.7", "ci-info": "^4.2.0", "cloudflare": "^4.4.1", "comment-json": "^4.5.1", "enquirer": "^2.4.1", "glob": "^12.0.0", "ts-tqdm": "^0.8.6", "yargs": "^18.0.0" }, "peerDependencies": { "next": ">=15.5.27 <16 || >=16.3.8", "rclone.js": "^0.6.6", "wrangler": "^4.125.0" }, "optionalPeers": ["rclone.js"], "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-7rrsBqd234GBBnHITobS0gagT4/fSfHJwm+ogYTyUb7ubg3PGC06biJgL9l4DZabFeu/ucqvml80sBr5torBgg=="],
|
||||
"@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.20.12", "", { "dependencies": { "@ast-grep/napi": "^0.45.3", "@dotenvx/dotenvx": "^2.33.0", "@opennextjs/aws": "4.1.11", "ci-info": "^4.2.0", "cloudflare": "^4.4.1", "comment-json": "^4.5.1", "enquirer": "^2.4.1", "glob": "^13.0.6", "ts-tqdm": "^0.8.6", "yargs": "^18.2.0" }, "peerDependencies": { "next": ">=15.5.28 <16 || >=16.4.1", "rclone.js": "^0.6.6", "wrangler": "^4.148.0" }, "optionalPeers": ["rclone.js"], "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-ZU1j7hMuOFaa0RHT/wrPXQKEEqzkkoc3fHaHy0Xrx4K0zZdSXGQbwbSHCnbjPRGXblCuWo+ur6UUkl0o9kVhrg=="],
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
|
||||
@@ -1354,7 +1351,7 @@
|
||||
|
||||
"@ts-morph/common": ["@ts-morph/common@0.11.1", "", { "dependencies": { "fast-glob": "^3.2.7", "minimatch": "^3.0.4", "mkdirp": "^1.0.4", "path-browserify": "^1.0.1" } }, "sha512-7hWZS0NRpEsNV8vWJzg7FEz6V8MaLNeJOmwmghqUXTpzk16V1LLZhdo+4QvE/+zv4cVci0OviuJFnqhEfoV3+g=="],
|
||||
|
||||
"@tsconfig/node18": ["@tsconfig/node18@1.0.3", "", {}, "sha512-RbwvSJQsuN9TB04AQbGULYfOGE/RnSFk/FLQ5b0NmDf5Kx2q/lABZbHQPKCO1vZ6Fiwkplu+yb9pGdLy1iGseQ=="],
|
||||
"@tsconfig/node18": ["@tsconfig/node18@18.2.7", "", {}, "sha512-UyenTORreCUh8MKh2qbLGGHTRrxjBbSwvX0ZoWs6Jx1jrNS62PXEA3+VaLvmb0YDQrgGU3v3h7zB9Xs/BN1ozg=="],
|
||||
|
||||
"@tsconfig/node20": ["@tsconfig/node20@20.1.6", "", {}, "sha512-sz+Hqx9zwZDpZIV871WSbUzSqNIsXzghZydypnfgzPKLltVJfkINfUeTct31n/tTSa9ZE1ZOfKdRre1uHHquYQ=="],
|
||||
|
||||
@@ -1698,7 +1695,7 @@
|
||||
|
||||
"bowser": ["bowser@2.11.0", "", {}, "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
|
||||
"brace-expansion": ["brace-expansion@5.0.12", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ=="],
|
||||
|
||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
|
||||
@@ -1800,7 +1797,7 @@
|
||||
|
||||
"convict": ["convict@6.2.5", "", { "dependencies": { "lodash.clonedeep": "^4.5.0", "yargs-parser": "^20.2.7" } }, "sha512-JtXpxqDqJ8P0UwEHwhxLzCIXQy97vlYBZR222Sbzb1q1Erex9ASrztJ29SyhWFQjod1AeFBaPzEEC8YvtZMIYg=="],
|
||||
|
||||
"cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
|
||||
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
||||
|
||||
"cookie-es": ["cookie-es@2.0.0", "", {}, "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg=="],
|
||||
|
||||
@@ -1966,8 +1963,6 @@
|
||||
|
||||
"domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
|
||||
|
||||
"dotenv": ["dotenv@16.4.7", "", {}, "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ=="],
|
||||
|
||||
"dts-resolver": ["dts-resolver@2.1.2", "", { "peerDependencies": { "oxc-resolver": ">=11.0.0" }, "optionalPeers": ["oxc-resolver"] }, "sha512-xeXHBQkn2ISSXxbJWD828PFjtyg+/UrMDo7W4Ffcs7+YWCquxU8YjV1KoxuiL+eJ5pg3ll+bC6flVv61L3LKZg=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
@@ -1980,8 +1975,6 @@
|
||||
|
||||
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
|
||||
|
||||
"eciesjs": ["eciesjs@0.4.13", "", { "dependencies": { "@ecies/ciphers": "^0.2.2", "@noble/ciphers": "^1.0.0", "@noble/curves": "^1.6.0", "@noble/hashes": "^1.5.0" } }, "sha512-zBdtR4K+wbj10bWPpIOF9DW+eFYQu8miU5ypunh0t4Bvt83ZPlEWgT5Dq/0G6uwEXumZKjfb5BZxYUZQ2Hzn/Q=="],
|
||||
|
||||
"edge-runtime": ["edge-runtime@2.5.9", "", { "dependencies": { "@edge-runtime/format": "2.2.1", "@edge-runtime/ponyfill": "2.4.2", "@edge-runtime/vm": "3.2.0", "async-listen": "3.0.1", "mri": "1.2.0", "picocolors": "1.0.0", "pretty-ms": "7.0.1", "signal-exit": "4.0.2", "time-span": "4.0.0" }, "bin": { "edge-runtime": "dist/cli/index.js" } }, "sha512-pk+k0oK0PVXdlT4oRp4lwh+unuKB7Ng4iZ2HB+EZ7QCEQizX360Rp/F4aRpgpRgdP2ufB35N+1KppHmYjqIGSg=="],
|
||||
|
||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||
@@ -2144,8 +2137,6 @@
|
||||
|
||||
"fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="],
|
||||
|
||||
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
@@ -2156,7 +2147,7 @@
|
||||
|
||||
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
||||
|
||||
"get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="],
|
||||
"get-east-asian-width": ["get-east-asian-width@1.7.0", "", {}, "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q=="],
|
||||
|
||||
"get-intrinsic": ["get-intrinsic@1.2.4", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" } }, "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ=="],
|
||||
|
||||
@@ -2174,7 +2165,7 @@
|
||||
|
||||
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
|
||||
|
||||
"glob": ["glob@12.0.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-5Qcll1z7IKgHr5g485ePDdHcNQY0k2dtv/bjYy0iuyGxQw2qSOiiXUXJ+AYQpg3HNoUMHqAruX478Jeev7UULw=="],
|
||||
"glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="],
|
||||
|
||||
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
@@ -2340,7 +2331,7 @@
|
||||
|
||||
"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=="],
|
||||
"jackspeak": ["jackspeak@2.3.6", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ=="],
|
||||
|
||||
"jiti": ["jiti@2.5.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w=="],
|
||||
|
||||
@@ -2604,15 +2595,15 @@
|
||||
|
||||
"miniflare": ["miniflare@4.20260401.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "^0.34.5", "undici": "7.24.4", "workerd": "1.20260401.1", "ws": "8.18.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-lngHPzZFN9sxYG/mhzvnWiBMNVAN5MsO/7g32ttJ07rymtiK/ZBalODTKb8Od+BQdlU5DOR4CjVt9NydjnUyYg=="],
|
||||
|
||||
"minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="],
|
||||
"minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="],
|
||||
|
||||
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
||||
"minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
|
||||
|
||||
"minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="],
|
||||
|
||||
"mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
|
||||
|
||||
"mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
|
||||
"mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="],
|
||||
|
||||
"mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="],
|
||||
|
||||
@@ -2638,7 +2629,7 @@
|
||||
|
||||
"netmask": ["netmask@2.0.2", "", {}, "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg=="],
|
||||
|
||||
"next": ["next@16.3.8", "", { "dependencies": { "@next/env": "16.3.8", "@swc/helpers": "0.5.23", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.5.23", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.3.8", "@next/swc-darwin-x64": "16.3.8", "@next/swc-linux-arm64-gnu": "16.3.8", "@next/swc-linux-arm64-musl": "16.3.8", "@next/swc-linux-x64-gnu": "16.3.8", "@next/swc-linux-x64-musl": "16.3.8", "@next/swc-win32-arm64-msvc": "16.3.8", "@next/swc-win32-x64-msvc": "16.3.8", "sharp": "^0.35.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-U7QEZaTini6wKrb8A8hqLLqYQyCetegKjCpJOyxk642vWoMoU1x5PyZCJFvgYgiptA8xc5j/9xYlZFO7w9Sjmw=="],
|
||||
"next": ["next@16.4.2", "", { "dependencies": { "@next/env": "16.4.2", "@swc/helpers": "0.5.23", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.5.23", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.4.2", "@next/swc-darwin-x64": "16.4.2", "@next/swc-linux-arm64-gnu": "16.4.2", "@next/swc-linux-arm64-musl": "16.4.2", "@next/swc-linux-x64-gnu": "16.4.2", "@next/swc-linux-x64-musl": "16.4.2", "@next/swc-win32-arm64-msvc": "16.4.2", "@next/swc-win32-x64-msvc": "16.4.2", "sharp": "^0.35.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-r+oBJojfefUjOiWkBhXgLS+igxX/7BWyK59uHnyOyplMSLtBZ+QLP3okrdYTbrfrbrmNTVWLBPaHDX9KRaIt3w=="],
|
||||
|
||||
"next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
|
||||
|
||||
@@ -2670,8 +2661,6 @@
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.2", "", {}, "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g=="],
|
||||
|
||||
"object-treeify": ["object-treeify@1.1.33", "", {}, "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A=="],
|
||||
|
||||
"obliterator": ["obliterator@1.6.1", "", {}, "sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig=="],
|
||||
|
||||
"on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="],
|
||||
@@ -2748,7 +2737,7 @@
|
||||
|
||||
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
|
||||
|
||||
"path-scurry": ["path-scurry@2.0.0", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg=="],
|
||||
"path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="],
|
||||
|
||||
"path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="],
|
||||
|
||||
@@ -2956,7 +2945,7 @@
|
||||
|
||||
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
||||
|
||||
"sharp": ["sharp@0.35.4", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.4", "@img/sharp-darwin-x64": "0.35.4", "@img/sharp-freebsd-wasm32": "0.35.4", "@img/sharp-libvips-darwin-arm64": "1.3.3", "@img/sharp-libvips-darwin-x64": "1.3.3", "@img/sharp-libvips-linux-arm": "1.3.3", "@img/sharp-libvips-linux-arm64": "1.3.3", "@img/sharp-libvips-linux-ppc64": "1.3.3", "@img/sharp-libvips-linux-riscv64": "1.3.3", "@img/sharp-libvips-linux-s390x": "1.3.3", "@img/sharp-libvips-linux-x64": "1.3.3", "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", "@img/sharp-libvips-linuxmusl-x64": "1.3.3", "@img/sharp-linux-arm": "0.35.4", "@img/sharp-linux-arm64": "0.35.4", "@img/sharp-linux-ppc64": "0.35.4", "@img/sharp-linux-riscv64": "0.35.4", "@img/sharp-linux-s390x": "0.35.4", "@img/sharp-linux-x64": "0.35.4", "@img/sharp-linuxmusl-arm64": "0.35.4", "@img/sharp-linuxmusl-x64": "0.35.4", "@img/sharp-webcontainers-wasm32": "0.35.4", "@img/sharp-win32-arm64": "0.35.4", "@img/sharp-win32-ia32": "0.35.4", "@img/sharp-win32-x64": "0.35.4" }, "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA=="],
|
||||
"sharp": ["sharp@0.35.5", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.5", "@img/sharp-darwin-x64": "0.35.5", "@img/sharp-freebsd-wasm32": "0.35.5", "@img/sharp-libvips-darwin-arm64": "1.3.4", "@img/sharp-libvips-darwin-x64": "1.3.4", "@img/sharp-libvips-linux-arm": "1.3.4", "@img/sharp-libvips-linux-arm64": "1.3.4", "@img/sharp-libvips-linux-ppc64": "1.3.4", "@img/sharp-libvips-linux-riscv64": "1.3.4", "@img/sharp-libvips-linux-s390x": "1.3.4", "@img/sharp-libvips-linux-x64": "1.3.4", "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", "@img/sharp-libvips-linuxmusl-x64": "1.3.4", "@img/sharp-linux-arm": "0.35.5", "@img/sharp-linux-arm64": "0.35.5", "@img/sharp-linux-ppc64": "0.35.5", "@img/sharp-linux-riscv64": "0.35.5", "@img/sharp-linux-s390x": "0.35.5", "@img/sharp-linux-x64": "0.35.5", "@img/sharp-linuxmusl-arm64": "0.35.5", "@img/sharp-linuxmusl-x64": "0.35.5", "@img/sharp-webcontainers-wasm32": "0.35.5", "@img/sharp-win32-arm64": "0.35.5", "@img/sharp-win32-ia32": "0.35.5", "@img/sharp-win32-x64": "0.35.5" }, "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
@@ -3076,7 +3065,7 @@
|
||||
|
||||
"term-size": ["term-size@2.2.1", "", {}, "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg=="],
|
||||
|
||||
"terser": ["terser@5.16.9", "", { "dependencies": { "@jridgewell/source-map": "^0.3.2", "acorn": "^8.5.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-HPa/FdTB9XGI2H1/keLFZHxl6WNvAI4YalHGtDQTlMnJcoqSab1UwL4l1hGEhs6/GmLHBZIg/YgB++jcbzoOEg=="],
|
||||
"terser": ["terser@5.36.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w=="],
|
||||
|
||||
"thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="],
|
||||
|
||||
@@ -3268,7 +3257,7 @@
|
||||
|
||||
"yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
|
||||
|
||||
"yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="],
|
||||
"yargs": ["yargs@18.2.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^8.2.1", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w=="],
|
||||
|
||||
"yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="],
|
||||
|
||||
@@ -3278,6 +3267,10 @@
|
||||
|
||||
"yauzl-promise": ["yauzl-promise@2.1.3", "", { "dependencies": { "yauzl": "^2.9.1", "yauzl-clone": "^1.0.4" } }, "sha512-A1pf6fzh6eYkK0L4Qp7g9jzJSDrM6nN0bOn5T0IbY4Yo3w+YkWlHFkJP7mzknMXjqusHFHlKsK2N+4OLsK2MRA=="],
|
||||
|
||||
"yocto-spinner": ["yocto-spinner@1.2.2", "", { "dependencies": { "yoctocolors": "^2.1.1" } }, "sha512-DODGl1wJjA/s5pnJFKau9lIYHT81lnhob1i3e1TjxZRxEhWRKl74nTbWE6H5KlkViQQTo/Z29YFdxzTZAMY3ng=="],
|
||||
|
||||
"yoctocolors": ["yoctocolors@2.2.0", "", {}, "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg=="],
|
||||
|
||||
"youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="],
|
||||
|
||||
"youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="],
|
||||
@@ -3370,17 +3363,7 @@
|
||||
|
||||
"@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="],
|
||||
|
||||
"@dotenvx/dotenvx/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
|
||||
|
||||
"@dotenvx/dotenvx/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
|
||||
|
||||
"@dotenvx/dotenvx/fdir": ["fdir@6.4.3", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw=="],
|
||||
|
||||
"@dotenvx/dotenvx/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"@dotenvx/dotenvx/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
|
||||
|
||||
"@dotenvx/dotenvx/which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
|
||||
"@dotenvx/dotenvx/undici": ["undici@6.29.1", "", {}, "sha512-EvRlUt/QqAEvXdQeRMYUfwOQsfzpDztykrG2owGEE/rBOBoYv9uF39ZzsNDh4lTFHhTr2XOZjKkYgueD12ZmGA=="],
|
||||
|
||||
"@floating-ui/react/@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.7", "", { "dependencies": { "@floating-ui/dom": "^1.7.5" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg=="],
|
||||
|
||||
@@ -3394,11 +3377,11 @@
|
||||
|
||||
"@hyperjump/json-schema/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
|
||||
|
||||
"@img/sharp-freebsd-wasm32/@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.4", "", { "dependencies": { "@emnapi/runtime": "^1.11.3" } }, "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA=="],
|
||||
"@img/sharp-freebsd-wasm32/@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.5", "", { "dependencies": { "@emnapi/runtime": "^1.11.3" } }, "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ=="],
|
||||
|
||||
"@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
||||
|
||||
"@img/sharp-webcontainers-wasm32/@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.4", "", { "dependencies": { "@emnapi/runtime": "^1.11.3" } }, "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA=="],
|
||||
"@img/sharp-webcontainers-wasm32/@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.5", "", { "dependencies": { "@emnapi/runtime": "^1.11.3" } }, "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ=="],
|
||||
|
||||
"@internationalized/date/@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
|
||||
|
||||
@@ -3410,6 +3393,8 @@
|
||||
|
||||
"@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
|
||||
|
||||
"@isaacs/fs-minipass/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
||||
|
||||
"@jridgewell/source-map/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.5", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg=="],
|
||||
|
||||
"@jridgewell/source-map/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
||||
@@ -3452,10 +3437,12 @@
|
||||
|
||||
"@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="],
|
||||
|
||||
"@node-minify/core/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="],
|
||||
"@node-minify/core/glob": ["glob@10.3.3", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^2.0.3", "minimatch": "^9.0.1", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", "path-scurry": "^1.10.1" }, "bin": { "glob": "dist/cjs/src/bin.js" } }, "sha512-92vPiMb/iqpmEgsOoIDvTjc50wf9CCCvMzsi6W0JLPeUKE8TWP1a73PgqSrqy7iAZxaSD1YdzU7QZR5LF51MJw=="],
|
||||
|
||||
"@opennextjs/aws/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
|
||||
|
||||
"@opennextjs/aws/yaml": ["yaml@2.9.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw=="],
|
||||
|
||||
"@oxc-transform/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=="],
|
||||
|
||||
"@playwright/test/playwright": ["playwright@1.61.1", "", { "dependencies": { "playwright-core": "1.61.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ=="],
|
||||
@@ -3596,6 +3583,8 @@
|
||||
|
||||
"@ts-morph/common/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||
|
||||
"@ts-morph/common/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
|
||||
|
||||
"@types/jsonwebtoken/@types/node": ["@types/node@20.16.11", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw=="],
|
||||
|
||||
"@types/swagger2openapi/@types/node": ["@types/node@20.16.11", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw=="],
|
||||
@@ -3650,6 +3639,8 @@
|
||||
|
||||
"@vercel/python-analysis/fs-extra": ["fs-extra@11.1.1", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ=="],
|
||||
|
||||
"@vercel/python-analysis/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="],
|
||||
|
||||
"@vercel/python-analysis/zod": ["zod@3.22.4", "", {}, "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg=="],
|
||||
|
||||
"@vercel/redwood/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
@@ -3694,7 +3685,7 @@
|
||||
|
||||
"body-parser/type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
|
||||
|
||||
"brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
"brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||
|
||||
"bun-types/@types/node": ["@types/node@20.12.14", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg=="],
|
||||
|
||||
@@ -3824,6 +3815,8 @@
|
||||
|
||||
"miniflare/undici": ["undici@7.24.4", "", {}, "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w=="],
|
||||
|
||||
"minizlib/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
||||
|
||||
"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=="],
|
||||
@@ -3906,7 +3899,7 @@
|
||||
|
||||
"swr/use-sync-external-store": ["use-sync-external-store@1.4.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw=="],
|
||||
|
||||
"terser/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
|
||||
"tar/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
||||
|
||||
"terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
||||
|
||||
@@ -3924,7 +3917,9 @@
|
||||
|
||||
"wrap-ansi/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
|
||||
|
||||
"yargs/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||
"yargs/string-width": ["string-width@8.3.0", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ=="],
|
||||
|
||||
"youch/cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
|
||||
|
||||
"zod-to-json-schema/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
||||
|
||||
@@ -4018,14 +4013,6 @@
|
||||
|
||||
"@cspotcode/source-map-support/@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
||||
|
||||
"@dotenvx/dotenvx/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
|
||||
|
||||
"@dotenvx/dotenvx/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="],
|
||||
|
||||
"@dotenvx/dotenvx/execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||
|
||||
"@dotenvx/dotenvx/which/isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@floating-ui/vue/@floating-ui/dom/@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="],
|
||||
@@ -4050,9 +4037,7 @@
|
||||
|
||||
"@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||
|
||||
"@node-minify/core/glob/minimatch": ["minimatch@8.0.4", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA=="],
|
||||
|
||||
"@node-minify/core/glob/minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="],
|
||||
"@node-minify/core/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"@node-minify/core/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
|
||||
|
||||
@@ -4162,12 +4147,14 @@
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="],
|
||||
|
||||
"@tailwindcss/oxide/tar/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
||||
|
||||
"@tailwindcss/oxide/tar/minizlib": ["minizlib@3.0.1", "", { "dependencies": { "minipass": "^7.0.4", "rimraf": "^5.0.5" } }, "sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg=="],
|
||||
|
||||
"@tailwindcss/oxide/tar/mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="],
|
||||
|
||||
"@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=="],
|
||||
@@ -4226,6 +4213,10 @@
|
||||
|
||||
"@vercel/nft/glob/minimatch": ["minimatch@10.1.2", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.1" } }, "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw=="],
|
||||
|
||||
"@vercel/nft/glob/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
||||
|
||||
"@vercel/nft/glob/path-scurry": ["path-scurry@2.0.0", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg=="],
|
||||
|
||||
"@vercel/node/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
||||
|
||||
"@vercel/python-analysis/fs-extra/jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="],
|
||||
@@ -4272,6 +4263,8 @@
|
||||
|
||||
"cliui/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="],
|
||||
|
||||
"cliui/string-width/get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="],
|
||||
|
||||
"cliui/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
||||
|
||||
"cloudflare/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
||||
@@ -4358,6 +4351,8 @@
|
||||
|
||||
"miniflare/sharp/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"multimatch/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
|
||||
|
||||
"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/dom/@floating-ui/core": ["@floating-ui/core@1.6.8", "", { "dependencies": { "@floating-ui/utils": "^0.2.8" } }, "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA=="],
|
||||
@@ -4382,6 +4377,8 @@
|
||||
|
||||
"rimraf/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"rimraf/glob/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
||||
|
||||
"rimraf/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
|
||||
|
||||
"side-channel-map/get-intrinsic/es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
@@ -4404,11 +4401,11 @@
|
||||
|
||||
"wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="],
|
||||
|
||||
"wrap-ansi/string-width/get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="],
|
||||
|
||||
"wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
||||
|
||||
"yargs/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="],
|
||||
|
||||
"yargs/string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
|
||||
"yargs/string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
|
||||
|
||||
"@argos-ci/core/sharp/@img/sharp-freebsd-wasm32/@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.3", "", { "dependencies": { "@emnapi/runtime": "^1.11.1" } }, "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w=="],
|
||||
|
||||
@@ -4470,12 +4467,16 @@
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@vercel/nft/glob/minimatch/@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.1", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ=="],
|
||||
|
||||
"@vercel/nft/glob/path-scurry/lru-cache": ["lru-cache@11.0.2", "", {}, "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA=="],
|
||||
|
||||
"@zenuml/core/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"@zenuml/core/tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||
@@ -4492,11 +4493,13 @@
|
||||
|
||||
"micro/raw-body/http-errors/toidentifier": ["toidentifier@1.0.0", "", {}, "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="],
|
||||
|
||||
"multimatch/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"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=="],
|
||||
"yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.4.0", "", {}, "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw=="],
|
||||
|
||||
"@argos-ci/core/sharp/@img/sharp-freebsd-wasm32/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="],
|
||||
|
||||
|
||||
@@ -95,6 +95,7 @@ const nextConfig = {
|
||||
GITBOOK_BLOCK_SEARCH_INDEXATION: process.env.GITBOOK_BLOCK_SEARCH_INDEXATION,
|
||||
GITBOOK_ALLOW_CUSTOMIZATION_OVERRIDE: process.env.GITBOOK_ALLOW_CUSTOMIZATION_OVERRIDE,
|
||||
GITBOOK_DISABLE_INSIGHTS: process.env.GITBOOK_DISABLE_INSIGHTS,
|
||||
GITBOOK_VISITOR_COUNTRY_HOSTNAMES: process.env.GITBOOK_VISITOR_COUNTRY_HOSTNAMES,
|
||||
VERCEL_TARGET_ENV: process.env.VERCEL_TARGET_ENV,
|
||||
|
||||
// Next.js envs
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
"@gitbook/react-openapi": "workspace:*",
|
||||
"@mermaid-js/mermaid-zenuml": "^0.2.2",
|
||||
"@modelcontextprotocol/sdk": "1.17.5",
|
||||
"@opennextjs/aws": "4.1.7",
|
||||
"@opennextjs/cloudflare": "1.20.8",
|
||||
"@opennextjs/aws": "4.1.11",
|
||||
"@opennextjs/cloudflare": "1.20.12",
|
||||
"@panzoom/panzoom": "^4.6.1",
|
||||
"@sindresorhus/fnv1a": "^3.1.0",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
@@ -51,7 +51,7 @@
|
||||
"micromark-extension-gfm": "^3.0.0",
|
||||
"motion": "^12.23.24",
|
||||
"negotiator": "^1.0.0",
|
||||
"next": "^16.3.8",
|
||||
"next": "^16.4.2",
|
||||
"next-themes": "^0.4.6",
|
||||
"nuqs": "^2.2.3",
|
||||
"object-hash": "^3.0.0",
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
GITBOOK_APP_URL,
|
||||
GITBOOK_ASSETS_URL,
|
||||
GITBOOK_DISABLE_INSIGHTS,
|
||||
GITBOOK_DISABLE_LOOKUP_ALTERNATIVES,
|
||||
GITBOOK_DISABLE_TRACKING,
|
||||
GITBOOK_FONTS_URL,
|
||||
GITBOOK_ICONS_URL,
|
||||
@@ -37,6 +38,7 @@ export async function GET(_req: NextRequest) {
|
||||
GITBOOK_INTEGRATIONS_CONTENT_HOST,
|
||||
GITBOOK_DISABLE_TRACKING,
|
||||
GITBOOK_DISABLE_INSIGHTS,
|
||||
GITBOOK_DISABLE_LOOKUP_ALTERNATIVES,
|
||||
|
||||
// Secret envs
|
||||
GITBOOK_SECRET: !!GITBOOK_SECRET,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client';
|
||||
import type { Assistant } from '../AI';
|
||||
import { useIsMobile } from '../hooks/useIsMobile';
|
||||
import { useTrackEvent } from '../Insights';
|
||||
import { Button } from '../primitives';
|
||||
import { KeyboardShortcut } from '../primitives/KeyboardShortcut';
|
||||
import { useLanguage } from '@/intl/client';
|
||||
@@ -19,7 +18,6 @@ export function AIChatButton(props: {
|
||||
}) {
|
||||
const { assistant, showLabel = true, withShortcut = true } = props;
|
||||
const language = useLanguage();
|
||||
const trackEvent = useTrackEvent();
|
||||
const isMobile = useIsMobile(MOBILE_BREAKPOINT, '[data-gb-header-content]');
|
||||
|
||||
return (
|
||||
@@ -41,12 +39,7 @@ export function AIChatButton(props: {
|
||||
</div>
|
||||
}
|
||||
aria-label={tString(language, 'ai_chat_ask', assistant.label)}
|
||||
onClick={() => {
|
||||
if (assistant.id === 'gitbook-assistant') {
|
||||
trackEvent({ type: 'ask_trigger_click', trigger: 'site' });
|
||||
}
|
||||
assistant.open(undefined, 'site');
|
||||
}}
|
||||
onClick={() => assistant.open(undefined, 'site')}
|
||||
>
|
||||
{showLabel ? t(language, 'ask') : null}
|
||||
</Button>
|
||||
|
||||
@@ -6,7 +6,6 @@ import { createPortal } from 'react-dom';
|
||||
|
||||
import { AIChatIcon } from '../AIChatIcon';
|
||||
import { useAIChatController, useAIConfig } from '@/components/AI';
|
||||
import { useTrackEvent } from '@/components/Insights';
|
||||
import { Button } from '@/components/primitives';
|
||||
import { isAIChatEnabled } from '@/components/utils/isAIChatEnabled';
|
||||
import { t, tString, useLanguage } from '@/intl/client';
|
||||
@@ -25,7 +24,6 @@ export function AskAIParagraphButton() {
|
||||
const config = useAIConfig();
|
||||
const language = useLanguage();
|
||||
const chatController = useAIChatController();
|
||||
const trackEvent = useTrackEvent();
|
||||
const [target, setTarget] = React.useState<HTMLElement | null>(null);
|
||||
|
||||
const enabled = isAIChatEnabled(config.aiMode);
|
||||
@@ -54,7 +52,6 @@ export function AskAIParagraphButton() {
|
||||
}
|
||||
|
||||
const onClick = () => {
|
||||
trackEvent({ type: 'ask_trigger_click', trigger: 'hover' });
|
||||
const text = getParagraphText(target).trim();
|
||||
if (!text) {
|
||||
return;
|
||||
|
||||
@@ -10,7 +10,6 @@ import { useStableTextSelection } from './useStableTextSelection';
|
||||
import { useAIChatController, useAIConfig } from '@/components/AI';
|
||||
import { useIsMobile } from '@/components/hooks/useIsMobile';
|
||||
import { useIsMounted } from '@/components/hooks/useIsMounted';
|
||||
import { useTrackEvent } from '@/components/Insights';
|
||||
import { Button } from '@/components/primitives';
|
||||
import { isAIChatEnabled } from '@/components/utils/isAIChatEnabled';
|
||||
import { t, useLanguage } from '@/intl/client';
|
||||
@@ -28,7 +27,6 @@ export function AskAITextSelection() {
|
||||
const config = useAIConfig();
|
||||
const language = useLanguage();
|
||||
const chatController = useAIChatController();
|
||||
const trackEvent = useTrackEvent();
|
||||
const isMobile = useIsMobile();
|
||||
const isMounted = useIsMounted();
|
||||
|
||||
@@ -73,7 +71,6 @@ export function AskAITextSelection() {
|
||||
}, [selection]);
|
||||
|
||||
const onClick = () => {
|
||||
trackEvent({ type: 'ask_trigger_click', trigger: 'selection' });
|
||||
if (!selection) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
ToolbarBody,
|
||||
ToolbarButton,
|
||||
ToolbarButtonGroup,
|
||||
type ToolbarButtonProps,
|
||||
ToolbarSubtitle,
|
||||
ToolbarTitle,
|
||||
} from './Toolbar';
|
||||
@@ -136,21 +137,14 @@ function ChangeRequestToolbar(props: ToolbarViewProps) {
|
||||
<Toolbar minified={minified} onMinifiedChange={onMinifiedChange}>
|
||||
<ToolbarBody>
|
||||
<ToolbarTitle
|
||||
prefix={`#${changeRequest.number}`}
|
||||
prefixLabel={`Change request #${changeRequest.number}`}
|
||||
suffix={changeRequest.subject || 'Untitled'}
|
||||
prefix={`Change #${changeRequest.number}:`}
|
||||
suffix={`${changeRequest.subject || 'Untitled'}`}
|
||||
/>
|
||||
<ToolbarSubtitle
|
||||
subtitle={
|
||||
<>
|
||||
<ToolbarDate value={changeRequest.updatedAt} />{' '}
|
||||
<motion.span
|
||||
layout="position"
|
||||
title={author}
|
||||
className="min-w-0 truncate"
|
||||
>
|
||||
by {author}
|
||||
</motion.span>
|
||||
<motion.span layout="position">by {author}</motion.span>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
@@ -345,8 +339,12 @@ function ToolbarActions(props: { children: React.ReactNode }) {
|
||||
);
|
||||
}
|
||||
|
||||
function EditPageButton(props: { href: string; siteId: string }) {
|
||||
const { href, siteId } = props;
|
||||
function EditPageButton(props: {
|
||||
href: string;
|
||||
siteId: string;
|
||||
motionValues?: ToolbarButtonProps['motionValues'];
|
||||
}) {
|
||||
const { href, motionValues, siteId } = props;
|
||||
const pagePath = useCurrentPagePath();
|
||||
|
||||
return (
|
||||
@@ -358,6 +356,7 @@ function EditPageButton(props: { href: string; siteId: string }) {
|
||||
buttonId: 'edit',
|
||||
})}
|
||||
icon="pen-to-square"
|
||||
motionValues={motionValues}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
.svgLogo {
|
||||
/* The toolbar is always dark, so the logo is always light. */
|
||||
--logo-fill: rgb(255 255 255 / 0.92);
|
||||
--logo-fill: var(--color-neutral-100);
|
||||
--seg-A-color: #2782c4;
|
||||
--seg-B-color: #43b7f2;
|
||||
--seg-C-color: #8be2ff;
|
||||
|
||||
@@ -11,14 +11,8 @@ export const AnimatedLogo: React.FC<AnimatedLogoProps> = (props) => {
|
||||
const { shouldAnimate = true } = props;
|
||||
|
||||
return (
|
||||
// Light even without the CSS module, which loads with the lazy toolbar chunk and can be
|
||||
// missing (e.g. after a hot reload): an SVG path without a fill renders black.
|
||||
<svg
|
||||
className={tcls(
|
||||
'text-white/90',
|
||||
styles.svgLogo,
|
||||
shouldAnimate ? undefined : styles.static
|
||||
)}
|
||||
className={tcls(styles.svgLogo, shouldAnimate ? undefined : styles.static)}
|
||||
width="28"
|
||||
height="28"
|
||||
viewBox="0 0 28 28"
|
||||
@@ -28,7 +22,6 @@ export const AnimatedLogo: React.FC<AnimatedLogoProps> = (props) => {
|
||||
<path
|
||||
d={dPath}
|
||||
transform="translate(14,14) scale(0.4)"
|
||||
fill="currentColor"
|
||||
strokeWidth={3}
|
||||
pathLength={1}
|
||||
strokeLinecap="round"
|
||||
|
||||
@@ -6,9 +6,8 @@ import type { SVGProps } from 'react';
|
||||
import { Icon, type IconName, IconStyle } from '@gitbook/icons';
|
||||
|
||||
import { DropdownMenu, DropdownMenuItem, DropdownMenuSeparator } from '../primitives';
|
||||
import { useToolbarSlot } from './Toolbar';
|
||||
import type { ToolbarButtonProps } from './Toolbar';
|
||||
import type { MinimalChangedPage, MinimalChangedPages } from './types';
|
||||
import { TOOLTIP_OFFSET } from './useMagnificationEffect';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
const STATUS_ICON_STYLES: Record<
|
||||
@@ -43,10 +42,12 @@ const STATUS_ICON_CHIP_CLASS = 'flex size-5 shrink-0 items-center justify-center
|
||||
/**
|
||||
* A button that opens a popover with quick links to the _changed_ pages.
|
||||
*/
|
||||
export function ChangedPagesButton(props: { changedPages: MinimalChangedPages | null }) {
|
||||
const { changedPages } = props;
|
||||
export function ChangedPagesButton(props: {
|
||||
changedPages: MinimalChangedPages | null;
|
||||
motionValues?: ToolbarButtonProps['motionValues'];
|
||||
}) {
|
||||
const { changedPages, motionValues } = props;
|
||||
const reduceMotion = useReducedMotion();
|
||||
const slot = useToolbarSlot();
|
||||
|
||||
if (!changedPages?.pages.length) {
|
||||
return null;
|
||||
@@ -58,19 +59,30 @@ export function ChangedPagesButton(props: { changedPages: MinimalChangedPages |
|
||||
}`;
|
||||
|
||||
const trigger = (
|
||||
<motion.button
|
||||
<button
|
||||
type="button"
|
||||
aria-label="View changed pages"
|
||||
style={
|
||||
reduceMotion || !slot
|
||||
? undefined
|
||||
: { scale: slot.scale, x: slot.x, transformOrigin: 'bottom center' }
|
||||
}
|
||||
className="relative size-8 cursor-pointer border-0 bg-transparent p-0"
|
||||
>
|
||||
<span
|
||||
<motion.span
|
||||
aria-hidden="true"
|
||||
style={
|
||||
reduceMotion
|
||||
? undefined
|
||||
: {
|
||||
scale: motionValues?.scale,
|
||||
x: motionValues?.x,
|
||||
transformOrigin: 'bottom center',
|
||||
zIndex: motionValues?.scale ? 10 : 'auto',
|
||||
}
|
||||
}
|
||||
transition={{
|
||||
type: 'spring',
|
||||
stiffness: 400,
|
||||
damping: 30,
|
||||
}}
|
||||
className={tcls(
|
||||
'toolbar-button',
|
||||
'relative flex size-8 items-center justify-center gap-1 truncate rounded-full text-sm transition-colors',
|
||||
'text-tint-7 hover:text-tint-1',
|
||||
'dark:text-tint-12',
|
||||
@@ -79,18 +91,14 @@ export function ChangedPagesButton(props: { changedPages: MinimalChangedPages |
|
||||
)}
|
||||
>
|
||||
<DiffIcon className="size-4 shrink-0" />
|
||||
</span>
|
||||
</motion.button>
|
||||
</motion.span>
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<DropdownMenu
|
||||
button={trigger}
|
||||
buttonTooltip="View changed pages"
|
||||
// Like the other toolbar buttons: against the unscaled slot, with room to magnify.
|
||||
buttonTooltipProps={
|
||||
slot ? { anchor: slot.anchorRef, sideOffset: TOOLTIP_OFFSET } : undefined
|
||||
}
|
||||
side="top"
|
||||
align="end"
|
||||
sideOffset={12}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useOnClickOutside } from 'usehooks-ts';
|
||||
|
||||
import { Icon, type IconName, IconStyle } from '@gitbook/icons';
|
||||
|
||||
import { ToolbarButton, useToolbarSlot } from './Toolbar';
|
||||
import { ToolbarButton, type ToolbarButtonProps } from './Toolbar';
|
||||
import styles from './Toolbar.module.css';
|
||||
import { useToolbarControls } from './ToolbarControlsContext';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
@@ -26,11 +26,15 @@ const ARC_PARAMS = {
|
||||
offsetAnchorY: 40,
|
||||
} as const;
|
||||
|
||||
interface HideToolbarButtonProps {
|
||||
motionValues?: ToolbarButtonProps['motionValues'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide menu trigger. Expands a macOS Dock-like submenu with 3 labeled actions.
|
||||
*/
|
||||
export function HideToolbarButton() {
|
||||
const slot = useToolbarSlot();
|
||||
export function HideToolbarButton(props: HideToolbarButtonProps) {
|
||||
const { motionValues } = props;
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [closing, setClosing] = React.useState(false);
|
||||
const controls = useToolbarControls();
|
||||
@@ -110,8 +114,11 @@ export function HideToolbarButton() {
|
||||
: null,
|
||||
].filter(Boolean) as ArcMenuItem[];
|
||||
|
||||
// The arc follows the button's push, but not its scale.
|
||||
const sharedMotionStyle = slot ? { x: slot.x } : undefined;
|
||||
const sharedMotionStyle = motionValues
|
||||
? {
|
||||
x: motionValues.x,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<ToolbarButton
|
||||
@@ -129,6 +136,7 @@ export function HideToolbarButton() {
|
||||
setOpen(true);
|
||||
}
|
||||
}}
|
||||
motionValues={motionValues}
|
||||
icon="gear"
|
||||
>
|
||||
{/* Expanding arc menu */}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
|
||||
import { ToolbarButton } from './Toolbar';
|
||||
import { ToolbarButton, type ToolbarButtonProps } from './Toolbar';
|
||||
|
||||
/**
|
||||
* Button to refresh the page if the content has been updated.
|
||||
*/
|
||||
export function RefreshContentButton(props: { refreshForUpdates: () => Promise<void> }) {
|
||||
const { refreshForUpdates } = props;
|
||||
export function RefreshContentButton(props: {
|
||||
refreshForUpdates: () => Promise<void>;
|
||||
motionValues?: ToolbarButtonProps['motionValues'];
|
||||
}) {
|
||||
const { refreshForUpdates, motionValues } = props;
|
||||
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
|
||||
@@ -31,6 +34,7 @@ export function RefreshContentButton(props: { refreshForUpdates: () => Promise<v
|
||||
refresh();
|
||||
}}
|
||||
disabled={loading}
|
||||
motionValues={motionValues}
|
||||
icon="rotate"
|
||||
iconClassName={loading ? 'animate-spin' : undefined}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
'use client';
|
||||
import { type MotionValue, motion, useReducedMotion, useSpring } from 'motion/react';
|
||||
import React from 'react';
|
||||
import {
|
||||
AnimatePresence,
|
||||
type MotionValue,
|
||||
motion,
|
||||
useReducedMotion,
|
||||
useSpring,
|
||||
} from 'motion/react';
|
||||
import React, { isValidElement } from 'react';
|
||||
|
||||
import { Icon, type IconName, IconStyle } from '@gitbook/icons';
|
||||
|
||||
@@ -9,7 +15,7 @@ import { AnimatedLogo } from './AnimatedLogo';
|
||||
import { useToolbarControls } from './ToolbarControlsContext';
|
||||
import { ToolbarVisibilityHint } from './ToolbarVisibilityHint';
|
||||
import { getCopyVariants, toolbarEasings } from './transitions';
|
||||
import { TOOLTIP_OFFSET, useMagnificationEffect } from './useMagnificationEffect';
|
||||
import { useMagnificationEffect } from './useMagnificationEffect';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
const DURATION_LOGO_APPEARANCE = 2000;
|
||||
@@ -102,93 +108,63 @@ export function Toolbar(props: ToolbarProps) {
|
||||
return (
|
||||
<motion.div
|
||||
data-testid="admin-toolbar"
|
||||
// Centered with `inset-x-0 mx-auto`: at `left-1/2` it could only grow to half the viewport,
|
||||
// which pushed the logo out of the pill.
|
||||
className="fixed inset-x-0 bottom-5 z-40 mx-auto w-fit max-w-xl px-4"
|
||||
className="fixed bottom-5 left-1/2 z-40 w-auto max-w-xl -translate-x-1/2 transform px-4"
|
||||
>
|
||||
<ToolbarVisibilityHint show={showHint} />
|
||||
|
||||
<motion.div
|
||||
onClick={() => {
|
||||
if (minified) {
|
||||
setShouldAnimateLogo(false);
|
||||
onMinifiedChange(false);
|
||||
}
|
||||
}}
|
||||
// Only minifying and expanding animate: content width changes (e.g. the date
|
||||
// format) would otherwise run a bouncy scale animation that stretches the content.
|
||||
layout
|
||||
layoutDependency={minified}
|
||||
onLayoutAnimationComplete={() => {
|
||||
if (!minified) {
|
||||
setShowHint(true);
|
||||
}
|
||||
}}
|
||||
transition={toolbarEasings.spring}
|
||||
className={tcls(
|
||||
minified ? 'cursor-pointer' : 'pr-2 pl-3.5',
|
||||
'relative',
|
||||
'flex',
|
||||
'items-center',
|
||||
'justify-center',
|
||||
'min-h-11',
|
||||
'min-w-12',
|
||||
'h-12',
|
||||
'py-2',
|
||||
'origin-center',
|
||||
'border-[0.5px] border-neutral-5 border-solid dark:border-neutral-8',
|
||||
'bg-[var(--toolbar-bg)]'
|
||||
)}
|
||||
style={PILL_STYLE}
|
||||
>
|
||||
{/* Logo — double-click to minimize */}
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
layout
|
||||
layoutDependency={minified}
|
||||
onDoubleClick={(e) => {
|
||||
if (minified) return;
|
||||
e.stopPropagation();
|
||||
window.getSelection()?.removeAllRanges();
|
||||
onMinifiedChange(true);
|
||||
onClick={() => {
|
||||
if (minified) {
|
||||
setShouldAnimateLogo(false);
|
||||
onMinifiedChange(false);
|
||||
}
|
||||
}}
|
||||
layout
|
||||
onLayoutAnimationComplete={() => {
|
||||
if (!minified) {
|
||||
setShowHint(true);
|
||||
}
|
||||
}}
|
||||
transition={toolbarEasings.spring}
|
||||
className={tcls(
|
||||
minified ? 'cursor-pointer' : 'pr-2 pl-3.5',
|
||||
'relative',
|
||||
'flex',
|
||||
'items-center',
|
||||
'justify-center',
|
||||
'min-h-11',
|
||||
'min-w-12',
|
||||
'h-12',
|
||||
'py-2',
|
||||
'origin-center',
|
||||
'border-[0.5px] border-neutral-5 border-solid dark:border-neutral-8',
|
||||
'bg-[var(--toolbar-bg)]'
|
||||
)}
|
||||
style={PILL_STYLE}
|
||||
>
|
||||
<AnimatedLogo shouldAnimate={shouldAnimateLogo} />
|
||||
</motion.div>
|
||||
|
||||
{/* A layout child, so it's counter-scaled while the pill expands. */}
|
||||
{!minified ? (
|
||||
{/* Logo — double-click to minimize */}
|
||||
<motion.div
|
||||
layout
|
||||
layoutDependency={minified}
|
||||
className="flex min-w-0 items-center"
|
||||
onDoubleClick={(e) => {
|
||||
if (minified) return;
|
||||
e.stopPropagation();
|
||||
window.getSelection()?.removeAllRanges();
|
||||
onMinifiedChange(true);
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<AnimatedLogo shouldAnimate={shouldAnimateLogo} />
|
||||
</motion.div>
|
||||
) : null}
|
||||
</motion.div>
|
||||
|
||||
{!minified ? children : null}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ToolbarBody(props: { children: React.ReactNode }) {
|
||||
// Shrinks so a long title truncates, but keeps room for the longest date format.
|
||||
return <div className="flex min-w-40 flex-col items-start px-3">{props.children}</div>;
|
||||
}
|
||||
|
||||
const MAGNIFICATION_SPRING = { stiffness: 400, damping: 30 };
|
||||
|
||||
/** A button's magnification, and the unscaled box to place its tooltip against. */
|
||||
type ToolbarSlot = {
|
||||
scale: MotionValue<number>;
|
||||
x: MotionValue<number>;
|
||||
anchorRef: React.RefObject<HTMLDivElement | null>;
|
||||
};
|
||||
|
||||
const ToolbarSlotContext = React.createContext<ToolbarSlot | null>(null);
|
||||
|
||||
/** The magnification of the toolbar button group slot this is rendered in, if any. */
|
||||
export function useToolbarSlot() {
|
||||
return React.useContext(ToolbarSlotContext);
|
||||
return <div className="flex flex-col items-start px-3">{props.children}</div>;
|
||||
}
|
||||
|
||||
export function ToolbarButtonGroup(props: { children: React.ReactNode }) {
|
||||
@@ -196,8 +172,8 @@ export function ToolbarButtonGroup(props: { children: React.ReactNode }) {
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const buttonChildren = React.Children.toArray(children).filter((child) => !!child);
|
||||
const { motionValues } = useMagnificationEffect({
|
||||
slotCount: buttonChildren.length,
|
||||
const { buttonMotionValues } = useMagnificationEffect({
|
||||
childrenCount: buttonChildren.length,
|
||||
containerRef,
|
||||
});
|
||||
|
||||
@@ -207,48 +183,28 @@ export function ToolbarButtonGroup(props: { children: React.ReactNode }) {
|
||||
variants={toolbarEasings.parent}
|
||||
initial="hidden"
|
||||
animate="show"
|
||||
// `relative`: the magnification measures slots by their `offsetLeft` in here.
|
||||
className="relative flex items-center gap-1 overflow-visible pl-4 pr-2"
|
||||
className="flex items-center gap-1 overflow-visible pl-4 pr-2"
|
||||
>
|
||||
{buttonChildren.map((child, index) => {
|
||||
const values = motionValues[index];
|
||||
return values ? (
|
||||
<ToolbarSlot
|
||||
key={React.isValidElement(child) && child.key ? child.key : index}
|
||||
scale={values.scale}
|
||||
x={values.x}
|
||||
>
|
||||
{child}
|
||||
</ToolbarSlot>
|
||||
) : null;
|
||||
const childEl = child as React.ReactElement;
|
||||
const childKey = childEl.key ?? `toolbar-button-${index}`;
|
||||
return (
|
||||
<ToolbarButtonWrapper
|
||||
key={childKey}
|
||||
child={childEl}
|
||||
rawMotionValues={buttonMotionValues[index]}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolbarSlot(props: {
|
||||
scale: MotionValue<number>;
|
||||
x: MotionValue<number>;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const anchorRef = React.useRef<HTMLDivElement>(null);
|
||||
const scale = useSpring(props.scale, MAGNIFICATION_SPRING);
|
||||
const x = useSpring(props.x, MAGNIFICATION_SPRING);
|
||||
const slot = React.useMemo(() => ({ scale, x, anchorRef }), [scale, x]);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
ref={anchorRef}
|
||||
variants={toolbarEasings.staggeringChild}
|
||||
// Buttons can render null (e.g. no changed pages): don't leave a gap in the group.
|
||||
className="relative empty:hidden"
|
||||
>
|
||||
<ToolbarSlotContext.Provider value={slot}>{props.children}</ToolbarSlotContext.Provider>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
export interface ToolbarButtonProps extends Omit<React.HTMLProps<HTMLAnchorElement>, 'title'> {
|
||||
motionValues?: {
|
||||
scale: MotionValue<number>;
|
||||
x: MotionValue<number>;
|
||||
};
|
||||
icon: IconName;
|
||||
iconClassName?: string;
|
||||
title?: React.ReactNode;
|
||||
@@ -256,10 +212,19 @@ export interface ToolbarButtonProps extends Omit<React.HTMLProps<HTMLAnchorEleme
|
||||
}
|
||||
|
||||
export const ToolbarButton = React.forwardRef<HTMLDivElement, ToolbarButtonProps>((props, ref) => {
|
||||
const { title, disabled, className, style, href, onClick, icon, iconClassName, children } =
|
||||
props;
|
||||
const {
|
||||
title,
|
||||
disabled,
|
||||
motionValues,
|
||||
className,
|
||||
style,
|
||||
href,
|
||||
onClick,
|
||||
icon,
|
||||
iconClassName,
|
||||
children,
|
||||
} = props;
|
||||
const reduceMotion = useReducedMotion();
|
||||
const slot = useToolbarSlot();
|
||||
|
||||
const anchor = (
|
||||
<motion.a
|
||||
@@ -268,11 +233,23 @@ export const ToolbarButton = React.forwardRef<HTMLDivElement, ToolbarButtonProps
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={
|
||||
reduceMotion || !slot
|
||||
? style
|
||||
: { scale: slot.scale, x: slot.x, transformOrigin: 'bottom center', ...style }
|
||||
reduceMotion
|
||||
? undefined
|
||||
: {
|
||||
scale: motionValues?.scale,
|
||||
x: motionValues?.x,
|
||||
transformOrigin: 'bottom center',
|
||||
zIndex: motionValues?.scale ? 10 : 'auto',
|
||||
...style,
|
||||
}
|
||||
}
|
||||
transition={{
|
||||
type: 'spring',
|
||||
stiffness: 400,
|
||||
damping: 30,
|
||||
}}
|
||||
className={tcls(
|
||||
'toolbar-button',
|
||||
className,
|
||||
'relative flex size-8 cursor-pointer items-center justify-center gap-1 truncate rounded-full text-sm transition-colors',
|
||||
'text-tint-7 hover:text-tint-1',
|
||||
@@ -285,68 +262,75 @@ export const ToolbarButton = React.forwardRef<HTMLDivElement, ToolbarButtonProps
|
||||
<Icon
|
||||
icon={icon}
|
||||
iconStyle={IconStyle.Solid}
|
||||
className={tcls('size-3.5 shrink-0', iconClassName)}
|
||||
className={tcls('size-3.5 shrink-0 group-hover:scale-110', iconClassName)}
|
||||
/>
|
||||
</motion.a>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative" ref={ref}>
|
||||
<motion.div variants={toolbarEasings.staggeringChild} className="relative" ref={ref}>
|
||||
{children ? children : null}
|
||||
{/* Anchored to the unscaled slot with room for full magnification, so the gap doesn't
|
||||
depend on how far the spring has grown when the tooltip opens. */}
|
||||
{title ? (
|
||||
<Tooltip
|
||||
label={title}
|
||||
anchor={slot?.anchorRef}
|
||||
sideOffset={slot ? TOOLTIP_OFFSET : 4}
|
||||
>
|
||||
{anchor}
|
||||
</Tooltip>
|
||||
) : (
|
||||
anchor
|
||||
)}
|
||||
</div>
|
||||
{title ? <Tooltip label={title}>{anchor}</Tooltip> : anchor}
|
||||
</motion.div>
|
||||
);
|
||||
});
|
||||
|
||||
ToolbarButton.displayName = 'ToolbarButton';
|
||||
|
||||
export function ToolbarTitle(props: {
|
||||
prefix?: string;
|
||||
/** What a short prefix stands for, e.g. "Change request #54" for "#54". */
|
||||
prefixLabel?: string;
|
||||
suffix: string;
|
||||
function ToolbarButtonWrapper(props: {
|
||||
child: React.ReactElement;
|
||||
rawMotionValues?: { scale: MotionValue<number>; x: MotionValue<number> };
|
||||
}) {
|
||||
const { prefix, prefixLabel, suffix } = props;
|
||||
// One tooltip for the whole title: the short prefix spelled out, and the full, often
|
||||
// truncated, subject.
|
||||
const label = prefixLabel ? (
|
||||
<span className="flex flex-col">
|
||||
<span className="opacity-70 text-xxs">{prefixLabel}</span>
|
||||
<span className="font-medium">{suffix}</span>
|
||||
</span>
|
||||
) : prefix ? (
|
||||
`${prefix}${suffix}`
|
||||
) : (
|
||||
suffix
|
||||
);
|
||||
const { child, rawMotionValues } = props;
|
||||
|
||||
// Convert the raw motion values to smooth spring easings
|
||||
const springScale = useSpring(rawMotionValues?.scale.get() ?? 1, {
|
||||
stiffness: 400,
|
||||
damping: 30,
|
||||
});
|
||||
const springX = useSpring(rawMotionValues?.x.get() ?? 0, { stiffness: 400, damping: 30 });
|
||||
|
||||
// Sync springs with raw motion values
|
||||
React.useEffect(() => {
|
||||
if (!rawMotionValues) return;
|
||||
|
||||
const unsubScale = rawMotionValues.scale.on('change', (v) => springScale.set(v));
|
||||
const unsubX = rawMotionValues.x.on('change', (v) => springX.set(v));
|
||||
|
||||
return () => {
|
||||
unsubScale();
|
||||
unsubX();
|
||||
};
|
||||
}, [rawMotionValues, springScale, springX]);
|
||||
|
||||
const motionValues = {
|
||||
scale: springScale,
|
||||
x: springX,
|
||||
};
|
||||
|
||||
if (!isValidElement<{ motionValues: typeof motionValues }>(child)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return React.cloneElement(child, {
|
||||
motionValues,
|
||||
});
|
||||
}
|
||||
|
||||
export function ToolbarTitle(props: { prefix?: string; suffix: string }) {
|
||||
return (
|
||||
<Tooltip label={label}>
|
||||
<div className="flex min-w-0 max-w-full items-center gap-1 text-xs">
|
||||
{prefix ? <ToolbarTitlePrefix title={prefix} /> : null}
|
||||
<ToolbarTitleSuffix title={suffix} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div className="flex items-center gap-1 text-xs">
|
||||
{props.prefix ? <ToolbarTitlePrefix title={props.prefix} /> : null}
|
||||
<ToolbarTitleSuffix title={props.suffix} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolbarTitlePrefix(props: { title: string }) {
|
||||
// Kept whole: prefixes are short, and the suffix is the one to truncate.
|
||||
return (
|
||||
<motion.span
|
||||
{...getCopyVariants(0)}
|
||||
className="shrink-0 whitespace-nowrap font-medium text-neutral-1 dark:text-neutral-12"
|
||||
className="truncate font-medium text-neutral-1 dark:text-neutral-12"
|
||||
>
|
||||
{props.title}
|
||||
</motion.span>
|
||||
@@ -357,7 +341,7 @@ function ToolbarTitleSuffix(props: { title: string }) {
|
||||
return (
|
||||
<motion.span
|
||||
{...getCopyVariants(1)}
|
||||
className="min-w-0 max-w-[28ch] truncate font-bold text-neutral-1 dark:text-neutral-12"
|
||||
className="max-w-[20ch] truncate font-bold text-neutral-1 dark:text-neutral-12"
|
||||
>
|
||||
{props.title}
|
||||
</motion.span>
|
||||
@@ -368,7 +352,7 @@ export function ToolbarSubtitle(props: { subtitle: React.ReactNode }) {
|
||||
return (
|
||||
<motion.span
|
||||
{...getCopyVariants(1)}
|
||||
className="text-neutral-1/80 dark:text-neutral-12/80 flex min-w-0 max-w-full items-center gap-1 whitespace-nowrap text-xxs"
|
||||
className="text-neutral-1/80 dark:text-neutral-12/80 inline-flex items-center gap-1 text-xxs"
|
||||
>
|
||||
{props.subtitle}
|
||||
</motion.span>
|
||||
|
||||
@@ -35,8 +35,7 @@ export function ToolbarDate(props: { value: string }) {
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
// Never wraps: the longer formats push the author's name to truncate instead.
|
||||
className="inline-flex shrink-0 items-center gap-1 whitespace-nowrap"
|
||||
className="inline-flex items-center gap-1"
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
>
|
||||
@@ -76,7 +75,7 @@ export function ToolbarDate(props: { value: string }) {
|
||||
setFormatIndex((i) => (i + 1) % DATE_FORMATS.length);
|
||||
}}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 25 }}
|
||||
className="relative inline-flex cursor-pointer select-none overflow-hidden whitespace-nowrap font-semibold transition-colors hover:text-white" // `text-white` is used insead of dark-mode adapting values because the date is always in a dark-tinted toolbar
|
||||
className="relative inline-flex cursor-pointer select-none overflow-hidden font-semibold transition-colors hover:text-white" // `text-white` is used insead of dark-mode adapting values because the date is always in a dark-tinted toolbar
|
||||
>
|
||||
<AnimatePresence mode="popLayout" initial={false}>
|
||||
<motion.span
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type MotionValue, motionValue, useReducedMotion } from 'motion/react';
|
||||
import { type MotionValue, motionValue } from 'motion/react';
|
||||
import React from 'react';
|
||||
|
||||
interface MagnificationConfig {
|
||||
@@ -16,29 +16,40 @@ interface MagnificationConfig {
|
||||
padding?: number;
|
||||
}
|
||||
|
||||
export const BUTTON_SIZE = 32;
|
||||
export const MAX_SCALE = 1.25;
|
||||
/** Tooltip gap above a slot, with room for the button fully magnified (it grows from the bottom). */
|
||||
export const TOOLTIP_OFFSET = 4 + BUTTON_SIZE * (MAX_SCALE - 1);
|
||||
|
||||
const defaultConfig: Required<MagnificationConfig> = {
|
||||
buttonSize: BUTTON_SIZE, // Standard button size for 32px (size-8) buttons
|
||||
maxScale: MAX_SCALE, // 25% scale increase - noticeable but not overwhelming
|
||||
buttonSize: 32, // Standard button size for 32px (size-8) buttons
|
||||
maxScale: 1.25, // 25% scale increase - noticeable but not overwhelming
|
||||
influenceRadius: 80, // ~2.5 button widths of influence
|
||||
spacingMultiplier: 1.3, // Creates proportional spacing to scale increase
|
||||
scaleExponent: 2.5, // Exponential curve for dramatic close-range scaling
|
||||
padding: 10, // Small buffer zone around container edges
|
||||
};
|
||||
|
||||
type SlotMotionValues = { scale: MotionValue<number>; x: MotionValue<number> };
|
||||
|
||||
const resetMotionValues = (motionValues: SlotMotionValues[]) => {
|
||||
const resetMotionValues = (
|
||||
motionValues: { scale: MotionValue<number>; x: MotionValue<number> }[]
|
||||
) => {
|
||||
motionValues.forEach(({ scale, x }) => {
|
||||
scale.set(1);
|
||||
x.set(0);
|
||||
});
|
||||
};
|
||||
|
||||
const captureButtonPositions = (buttons: HTMLElement[]) => {
|
||||
// Reset transforms to get original positions
|
||||
buttons.forEach((button) => {
|
||||
button.style.transform = '';
|
||||
});
|
||||
return buttons.map((button) => {
|
||||
const rect = button.getBoundingClientRect();
|
||||
return {
|
||||
left: rect.left,
|
||||
width: rect.width,
|
||||
top: rect.top,
|
||||
height: rect.height,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const calculateScale = (
|
||||
mouseX: number,
|
||||
buttonCenterX: number,
|
||||
@@ -51,7 +62,8 @@ const calculateScale = (
|
||||
if (distance > config.influenceRadius) return 1;
|
||||
|
||||
// Calculate distance from container edge (X-axis only)
|
||||
const distanceFromEdge = Math.min(mouseX - containerRect.left, containerRect.right - mouseX);
|
||||
const distanceFromEdgeX = Math.min(mouseX - containerRect.left, containerRect.right - mouseX);
|
||||
const distanceFromEdge = distanceFromEdgeX;
|
||||
|
||||
// Define the "heart" zone - 8px from center (16x16px total area)
|
||||
const heartZoneRadius = 8;
|
||||
@@ -82,20 +94,24 @@ const calculateScale = (
|
||||
|
||||
const calculateSpacing = (
|
||||
buttonIndex: number,
|
||||
scales: number[],
|
||||
centers: (number | null)[],
|
||||
buttonEffects: { scale: number; translateX: number }[],
|
||||
positions: { left: number; width: number; top: number; height: number }[],
|
||||
config: Required<MagnificationConfig>
|
||||
) => {
|
||||
const buttonCenterX = centers[buttonIndex];
|
||||
if (buttonCenterX == null) return 0;
|
||||
const currentPos = positions[buttonIndex];
|
||||
if (!currentPos) return 0;
|
||||
|
||||
const buttonCenterX = currentPos.left + currentPos.width / 2;
|
||||
let leftPush = 0;
|
||||
let rightPush = 0;
|
||||
|
||||
scales.forEach((otherScale, otherIndex) => {
|
||||
const otherCenterX = centers[otherIndex];
|
||||
if (otherIndex === buttonIndex || otherCenterX == null) return;
|
||||
buttonEffects.forEach((otherEffect, otherIndex) => {
|
||||
if (otherIndex === buttonIndex) return;
|
||||
|
||||
const otherPos = positions[otherIndex];
|
||||
if (!otherPos) return;
|
||||
|
||||
const otherCenterX = otherPos.left + otherPos.width / 2;
|
||||
const distanceBetween = Math.abs(buttonCenterX - otherCenterX);
|
||||
|
||||
// Only apply influence if close enough
|
||||
@@ -104,7 +120,7 @@ const calculateSpacing = (
|
||||
0,
|
||||
1 - distanceBetween / (config.influenceRadius * 1.5)
|
||||
);
|
||||
const extraSpace = (otherScale - 1) * config.buttonSize;
|
||||
const extraSpace = (otherEffect.scale - 1) * config.buttonSize;
|
||||
const pushAmount = (extraSpace / 2) * config.spacingMultiplier * influenceStrength;
|
||||
|
||||
if (otherCenterX < buttonCenterX) {
|
||||
@@ -118,88 +134,104 @@ const calculateSpacing = (
|
||||
return rightPush - leftPush;
|
||||
};
|
||||
|
||||
/**
|
||||
* Dock-like magnification of the slots (direct children) of `containerRef`. Returns raw motion
|
||||
* values per slot index, meant to be smoothed with springs by the slots.
|
||||
*/
|
||||
export function useMagnificationEffect(props: {
|
||||
slotCount: number;
|
||||
childrenCount: number;
|
||||
containerRef: React.RefObject<HTMLElement | null>;
|
||||
config?: MagnificationConfig;
|
||||
}) {
|
||||
const { slotCount, containerRef, config } = props;
|
||||
const reduceMotion = useReducedMotion();
|
||||
const { childrenCount, containerRef, config } = props;
|
||||
const originalPositionsRef = React.useRef<
|
||||
{ left: number; width: number; top: number; height: number }[]
|
||||
>([]);
|
||||
|
||||
const finalConfig = React.useMemo(() => ({ ...defaultConfig, ...config }), [config]);
|
||||
|
||||
// Stable across renders, grown as slots are added, so existing springs keep their source.
|
||||
const motionValuesRef = React.useRef<SlotMotionValues[]>([]);
|
||||
while (motionValuesRef.current.length < slotCount) {
|
||||
motionValuesRef.current.push({ scale: motionValue(1), x: motionValue(0) });
|
||||
}
|
||||
const motionValues = motionValuesRef.current;
|
||||
// Create basic motion values that will be consumed by springs in the components
|
||||
const buttonMotionValues = React.useMemo(() => {
|
||||
return Array.from({ length: childrenCount }, () => ({
|
||||
scale: motionValue(1),
|
||||
x: motionValue(0),
|
||||
}));
|
||||
}, [childrenCount]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container || reduceMotion) {
|
||||
resetMotionValues(motionValues);
|
||||
if (!container) return;
|
||||
|
||||
const buttons = Array.from(container.querySelectorAll('.toolbar-button')) as HTMLElement[];
|
||||
|
||||
if (buttons.length !== childrenCount) {
|
||||
console.error(
|
||||
`Button count (${buttons.length}) does not match children count (${childrenCount})`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let frame = 0;
|
||||
let pointer = { x: 0, y: 0 };
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
const buttons = Array.from(
|
||||
container.querySelectorAll('.toolbar-button')
|
||||
) as HTMLElement[];
|
||||
|
||||
// Measured every frame from layout positions (`offsetLeft`), which the slots' transforms
|
||||
// don't affect, so nothing goes stale when the toolbar resizes, re-centers or animates in.
|
||||
const update = () => {
|
||||
frame = 0;
|
||||
const rect = container.getBoundingClientRect();
|
||||
const inRange =
|
||||
pointer.x >= rect.left - finalConfig.padding &&
|
||||
pointer.x <= rect.right + finalConfig.padding &&
|
||||
pointer.y >= rect.top - finalConfig.padding &&
|
||||
pointer.y <= rect.bottom + finalConfig.padding;
|
||||
if (!inRange) {
|
||||
resetMotionValues(motionValues);
|
||||
// Capture positions if needed
|
||||
if (originalPositionsRef.current.length !== buttons.length) {
|
||||
originalPositionsRef.current = captureButtonPositions(buttons);
|
||||
}
|
||||
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const { clientX: mouseX, clientY: mouseY } = event;
|
||||
|
||||
// Check if mouse is in range
|
||||
const isInRange =
|
||||
mouseX >= containerRect.left - finalConfig.padding &&
|
||||
mouseX <= containerRect.right + finalConfig.padding &&
|
||||
mouseY >= containerRect.top - finalConfig.padding &&
|
||||
mouseY <= containerRect.bottom + finalConfig.padding;
|
||||
|
||||
if (!isInRange) {
|
||||
resetMotionValues(buttonMotionValues);
|
||||
return;
|
||||
}
|
||||
|
||||
const slots = Array.from(container.children) as HTMLElement[];
|
||||
// Empty or hidden slots take no space and don't magnify.
|
||||
const centers = slots.map((slot) =>
|
||||
slot.offsetWidth > 0 ? rect.left + slot.offsetLeft + slot.offsetWidth / 2 : null
|
||||
);
|
||||
const scales = centers.map((center) =>
|
||||
center == null ? 1 : calculateScale(pointer.x, center, rect, finalConfig)
|
||||
);
|
||||
motionValues.forEach((values, index) => {
|
||||
values.scale.set(scales[index] ?? 1);
|
||||
values.x.set(calculateSpacing(index, scales, centers, finalConfig));
|
||||
// Calculate scales for all buttons
|
||||
const buttonEffects = buttons.map((_, index) => {
|
||||
const pos = originalPositionsRef.current[index];
|
||||
if (!pos) return { scale: 1, translateX: 0 };
|
||||
|
||||
const buttonCenterX = pos.left + pos.width / 2;
|
||||
const scale = calculateScale(mouseX, buttonCenterX, containerRect, finalConfig);
|
||||
return { scale, translateX: 0 };
|
||||
});
|
||||
|
||||
// Calculate spacing for all buttons
|
||||
buttonEffects.forEach((effect, index) => {
|
||||
effect.translateX = calculateSpacing(
|
||||
index,
|
||||
buttonEffects,
|
||||
originalPositionsRef.current,
|
||||
finalConfig
|
||||
);
|
||||
});
|
||||
|
||||
// Update motion values
|
||||
buttonEffects.forEach((effect, index) => {
|
||||
const motionValue = buttonMotionValues[index];
|
||||
if (motionValue) {
|
||||
motionValue.scale.set(effect.scale);
|
||||
motionValue.x.set(effect.translateX);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onPointerMove = (event: PointerEvent) => {
|
||||
if (event.pointerType !== 'mouse') {
|
||||
return;
|
||||
}
|
||||
pointer = { x: event.clientX, y: event.clientY };
|
||||
if (!frame) {
|
||||
frame = requestAnimationFrame(update);
|
||||
}
|
||||
};
|
||||
const onPointerLeave = () => {
|
||||
cancelAnimationFrame(frame);
|
||||
frame = 0;
|
||||
resetMotionValues(motionValues);
|
||||
};
|
||||
const handleMouseLeave = () => resetMotionValues(buttonMotionValues);
|
||||
|
||||
container.addEventListener('mousemove', handleMouseMove);
|
||||
container.addEventListener('mouseleave', handleMouseLeave);
|
||||
|
||||
container.addEventListener('pointermove', onPointerMove);
|
||||
container.addEventListener('pointerleave', onPointerLeave);
|
||||
return () => {
|
||||
cancelAnimationFrame(frame);
|
||||
container.removeEventListener('pointermove', onPointerMove);
|
||||
container.removeEventListener('pointerleave', onPointerLeave);
|
||||
container.removeEventListener('mousemove', handleMouseMove);
|
||||
container.removeEventListener('mouseleave', handleMouseLeave);
|
||||
};
|
||||
}, [containerRef, finalConfig, motionValues, reduceMotion]);
|
||||
}, [finalConfig, containerRef, buttonMotionValues, childrenCount]);
|
||||
|
||||
return { motionValues };
|
||||
return { buttonMotionValues };
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { DocumentInlineAnnotation } from '@gitbook/api';
|
||||
import { Blocks } from '../Blocks';
|
||||
import type { InlineProps } from '../Inline';
|
||||
import { Inlines } from '../Inlines';
|
||||
import { getCommentMarkerProps } from '../utils';
|
||||
import { AnnotationPopover } from './AnnotationPopover';
|
||||
import { getNodeFragmentByType } from '@/lib/document';
|
||||
|
||||
@@ -22,19 +21,13 @@ export function Annotation(props: InlineProps<DocumentInlineAnnotation>) {
|
||||
return (
|
||||
<AnnotationPopover
|
||||
body={
|
||||
// The app doesn't allow comments inside annotation bodies.
|
||||
<div
|
||||
{...getCommentMarkerProps(context, 'data-comment-ignore')}
|
||||
className="contents"
|
||||
>
|
||||
<Blocks
|
||||
document={document}
|
||||
ancestorBlocks={[]}
|
||||
context={context}
|
||||
nodes={fragment.nodes}
|
||||
style={['contents']}
|
||||
/>
|
||||
</div>
|
||||
<Blocks
|
||||
document={document}
|
||||
ancestorBlocks={[]}
|
||||
context={context}
|
||||
nodes={fragment.nodes}
|
||||
style={['contents']}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{content}
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
|
||||
import type { BlockProps } from './Block';
|
||||
import { NotFoundRefHoverCard } from './NotFoundRefHoverCard';
|
||||
import { getCommentAnchorProps } from './utils';
|
||||
import { Card, type CardProps } from '@/components/primitives';
|
||||
import {
|
||||
type ResolvedContentRef,
|
||||
@@ -32,7 +31,6 @@ export async function BlockContentRef(props: BlockProps<DocumentBlockContentRef>
|
||||
return (
|
||||
<NotFoundRefHoverCard context={context}>
|
||||
<BlockContentRefCard
|
||||
{...getCommentAnchorProps(context, block.key)}
|
||||
contentRef={block.data.ref}
|
||||
resolved={fallback}
|
||||
style={style}
|
||||
@@ -41,14 +39,7 @@ export async function BlockContentRef(props: BlockProps<DocumentBlockContentRef>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BlockContentRefCard
|
||||
{...getCommentAnchorProps(context, block.key)}
|
||||
contentRef={block.data.ref}
|
||||
resolved={resolved}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
return <BlockContentRefCard contentRef={block.data.ref} resolved={resolved} style={style} />;
|
||||
}
|
||||
|
||||
function BlockContentRefCard(
|
||||
|
||||
@@ -9,7 +9,6 @@ import type {
|
||||
|
||||
import type { DocumentContextProps } from './DocumentView';
|
||||
import { Inlines } from './Inlines';
|
||||
import { getCommentAnchorProps } from './utils';
|
||||
import { getNodeFragmentByName, isNodeEmpty } from '@/lib/document';
|
||||
import { type ClassValue, tcls } from '@/lib/tailwind';
|
||||
|
||||
@@ -35,11 +34,6 @@ export function Caption(
|
||||
block: DocumentBlockImage | DocumentBlockDrawing | DocumentBlockEmbed | DocumentBlockFile;
|
||||
withBorder?: boolean;
|
||||
withFrame?: boolean;
|
||||
/**
|
||||
* Expose the block key for commenting; off inside an `images` block, which carries the key
|
||||
* itself since its images aren't comment targets.
|
||||
*/
|
||||
withBlockKey?: boolean;
|
||||
} & DocumentContextProps
|
||||
) {
|
||||
const {
|
||||
@@ -65,11 +59,8 @@ export function Caption(
|
||||
],
|
||||
style,
|
||||
align = 'center',
|
||||
withBlockKey = false,
|
||||
} = props;
|
||||
|
||||
const anchorProps = withBlockKey ? getCommentAnchorProps(context, block.key) : {};
|
||||
|
||||
const caption = getNodeFragmentByName(block, 'caption');
|
||||
const captionParagraph = caption?.nodes[0];
|
||||
|
||||
@@ -78,15 +69,11 @@ export function Caption(
|
||||
captionParagraph.type !== 'paragraph' ||
|
||||
isNodeEmpty(captionParagraph)
|
||||
) {
|
||||
return (
|
||||
<div {...anchorProps} className={tcls(style, wrapperStyle)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
return <div className={tcls(style, wrapperStyle)}>{children}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<picture {...anchorProps} className={tcls('relative', style)}>
|
||||
<picture className={tcls('relative', style)}>
|
||||
<div className={tcls(wrapperStyle, captionAlignStyles[align])}>{children}</div>
|
||||
<figcaption
|
||||
className={tcls(
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { getCodeTextFromId } from './utils';
|
||||
import { useAIChatController, useAIConfig } from '@/components/AI';
|
||||
import { AIChatIcon } from '@/components/AIChat';
|
||||
import { useTrackEvent } from '@/components/Insights';
|
||||
import { Button } from '@/components/primitives';
|
||||
import { isAIChatEnabled } from '@/components/utils/isAIChatEnabled';
|
||||
import { t, useLanguage } from '@/intl/client';
|
||||
@@ -20,14 +19,12 @@ export function AskAICodeButton(props: {
|
||||
const language = useLanguage();
|
||||
const config = useAIConfig();
|
||||
const chatController = useAIChatController();
|
||||
const trackEvent = useTrackEvent();
|
||||
|
||||
if (!isAIChatEnabled(config.aiMode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const onClick = () => {
|
||||
trackEvent({ type: 'ask_trigger_click', trigger: 'code' });
|
||||
const codeText = getCodeTextFromId(codeId);
|
||||
if (!codeText?.trim()) {
|
||||
return;
|
||||
@@ -40,7 +37,7 @@ export function AskAICodeButton(props: {
|
||||
content: codeText,
|
||||
syntax,
|
||||
});
|
||||
chatController.open('code');
|
||||
chatController.open();
|
||||
chatController.focus();
|
||||
};
|
||||
|
||||
|
||||
@@ -23,8 +23,6 @@ export type ClientBlockProps = Pick<BlockProps<DocumentBlockCode>, 'block' | 'st
|
||||
mode: BlockProps<DocumentBlockCode>['context']['mode'];
|
||||
themes?: CustomizationThemedCodeTheme;
|
||||
embedded?: boolean;
|
||||
/** Node key for comments, set only where commenting is possible. */
|
||||
commentKey?: string;
|
||||
};
|
||||
|
||||
export const CODE_BLOCK_DEFAULT_COLLAPSED_LINE_COUNT = 10;
|
||||
@@ -34,8 +32,7 @@ export const CODE_BLOCK_DEFAULT_COLLAPSED_LINE_COUNT = 10;
|
||||
* It allows us to defer some load to avoid blocking the rendering of the whole page with block highlighting.
|
||||
*/
|
||||
export function ClientCodeBlock(props: ClientBlockProps) {
|
||||
const { block, mode, style, inlines, inlineExprVariables, themes, embedded, commentKey } =
|
||||
props;
|
||||
const { block, mode, style, inlines, inlineExprVariables, themes, embedded } = props;
|
||||
const blockRef = useRef<HTMLDivElement>(null);
|
||||
const isInViewportRef = useRef(false);
|
||||
const [isInViewport, setIsInViewport] = useState(false);
|
||||
@@ -148,7 +145,6 @@ export function ClientCodeBlock(props: ClientBlockProps) {
|
||||
id={codeBlockBodyId}
|
||||
isPrint={mode === 'print'}
|
||||
embedded={embedded}
|
||||
commentKey={commentKey}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import type {
|
||||
|
||||
import type { BlockProps } from '../Block';
|
||||
import { Blocks } from '../Blocks';
|
||||
import { getCommentKeyProp, getCommentMarkerProps } from '../utils';
|
||||
import { ClientCodeBlock } from './ClientCodeBlock';
|
||||
import { CodeBlockRenderer } from './CodeBlockRenderer';
|
||||
import { highlight } from './highlight';
|
||||
@@ -60,20 +59,14 @@ export async function CodeBlock(
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
// The app doesn't allow comments inside annotation bodies.
|
||||
<div
|
||||
<Blocks
|
||||
key={index}
|
||||
{...getCommentMarkerProps(context, 'data-comment-ignore')}
|
||||
className="contents"
|
||||
>
|
||||
<Blocks
|
||||
document={document}
|
||||
ancestorBlocks={[]}
|
||||
context={context}
|
||||
nodes={fragment.nodes}
|
||||
style="space-y-4"
|
||||
/>
|
||||
</div>
|
||||
document={document}
|
||||
ancestorBlocks={[]}
|
||||
context={context}
|
||||
nodes={fragment.nodes}
|
||||
style="space-y-4"
|
||||
/>
|
||||
);
|
||||
})();
|
||||
|
||||
@@ -99,7 +92,6 @@ export async function CodeBlock(
|
||||
theme={theme}
|
||||
isPrint={context.mode === 'print'}
|
||||
embedded={embedded}
|
||||
{...getCommentKeyProp(context, block.key)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -122,7 +114,6 @@ export async function CodeBlock(
|
||||
mode: context.mode,
|
||||
themes,
|
||||
embedded,
|
||||
...getCommentKeyProp(context, block.key),
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -5,7 +5,6 @@ import type { DocumentBlockCode } from '@gitbook/api';
|
||||
|
||||
import { AnnotationPopover } from '../Annotation/AnnotationPopover';
|
||||
import type { BlockProps } from '../Block';
|
||||
import { getCommentKeyAttributes } from '../utils/commentAnchors';
|
||||
import { AskAICodeButton } from './AskAICodeButton';
|
||||
import { CopyCodeButton } from './CopyCodeButton';
|
||||
import type { HighlightLine, HighlightTheme, HighlightToken } from './highlight-tokens';
|
||||
@@ -13,8 +12,6 @@ import { tcls } from '@/lib/tailwind';
|
||||
|
||||
type CodeBlockRendererProps = Pick<BlockProps<DocumentBlockCode>, 'block' | 'style'> & {
|
||||
theme: HighlightTheme;
|
||||
/** Node key for comments, set only where commenting is possible. */
|
||||
commentKey?: string;
|
||||
'aria-busy'?: boolean;
|
||||
id?: string;
|
||||
/**
|
||||
@@ -32,7 +29,7 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer(
|
||||
props: CodeBlockRendererProps,
|
||||
ref: React.ForwardedRef<HTMLDivElement>
|
||||
) {
|
||||
const { block, style, theme, 'aria-busy': ariaBusy, isPrint, embedded, commentKey } = props;
|
||||
const { block, style, theme, 'aria-busy': ariaBusy, isPrint, embedded } = props;
|
||||
|
||||
const withLineNumbers = Boolean(block.data.lineNumbers) && block.nodes.length > 1;
|
||||
const withWrap = block.data.overflow === 'wrap';
|
||||
@@ -53,7 +50,6 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer(
|
||||
<div
|
||||
ref={ref}
|
||||
id={codeId}
|
||||
{...getCommentKeyAttributes(commentKey)}
|
||||
aria-busy={ariaBusy}
|
||||
className={tcls(
|
||||
'group/codeblock shiki relative flex shrink flex-col overflow-hidden print:overflow-visible',
|
||||
|
||||
@@ -218,12 +218,7 @@ export function MermaidCodeBlock(
|
||||
return (
|
||||
<>
|
||||
{/* Inline slot: hosts the diagram in the document flow until it goes fullscreen. */}
|
||||
<div
|
||||
ref={rootRef}
|
||||
data-block-key={props.commentKey}
|
||||
className={tcls('relative', style)}
|
||||
contentEditable={false}
|
||||
/>
|
||||
<div ref={rootRef} className={tcls('relative', style)} contentEditable={false} />
|
||||
{diagramHostRef.current ? createPortal(diagram, diagramHostRef.current) : null}
|
||||
<Dialog.Root
|
||||
open={isFullscreen}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { type DocumentBlockColumns, type Length, VerticalAlignment } from '@gitb
|
||||
|
||||
import type { BlockProps } from '../Block';
|
||||
import { Blocks } from '../Blocks';
|
||||
import { getCommentMarkerProps } from '../utils';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
export function Columns(props: BlockProps<DocumentBlockColumns>) {
|
||||
@@ -38,8 +37,6 @@ export function Columns(props: BlockProps<DocumentBlockColumns>) {
|
||||
|
||||
return (
|
||||
<div
|
||||
// The app doesn't allow comments inside columns.
|
||||
{...getCommentMarkerProps(context, 'data-comment-ignore')}
|
||||
className={tcls(
|
||||
'grid w-full @2xl:grid-flow-col @2xl:grid-cols-[repeat(var(--grid-slices),minmax(0,1fr))] grid-cols-1 @2xl:grid-rows-1 gap-x-8 gap-y-4',
|
||||
style
|
||||
|
||||
@@ -37,12 +37,6 @@ export interface DocumentContext {
|
||||
*/
|
||||
withLinkPreviews?: boolean;
|
||||
|
||||
/**
|
||||
* Render the markers comments anchor to (block keys, table cells). Only where commenting is
|
||||
* possible, so public pages don't carry them.
|
||||
*/
|
||||
withCommentAnchors?: boolean;
|
||||
|
||||
/**
|
||||
* True when this document is the main page body — the only place a background page cover is
|
||||
* shown behind the content. Cover-aware text styling (contrast over the cover) is scoped to it
|
||||
|
||||
@@ -18,7 +18,7 @@ export async function Drawing(props: BlockProps<DocumentBlockDrawing>) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Caption {...props} withBlockKey>
|
||||
<Caption {...props}>
|
||||
<Image
|
||||
sources={{
|
||||
light: {
|
||||
|
||||
@@ -32,13 +32,15 @@ export async function Embed(props: BlockProps<gitbookAPI.DocumentBlockEmbed>) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Caption {...props} withBorder withBlockKey>
|
||||
<Caption {...props} withBorder>
|
||||
{embed.type === 'rich' ? (
|
||||
<>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html:
|
||||
context.mode !== 'print' ? lazyLoadIframes(embed.html) : embed.html,
|
||||
context.mode !== 'print' && shouldLazyLoad(block.data.url)
|
||||
? lazyLoadIframes(embed.html)
|
||||
: embed.html,
|
||||
}}
|
||||
data-visual-test="blackout"
|
||||
/>
|
||||
@@ -75,14 +77,22 @@ export async function Embed(props: BlockProps<gitbookAPI.DocumentBlockEmbed>) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Defer players until they are visible. Pages with many embeds exhaust the browser's request
|
||||
* budget (ERR_INSUFFICIENT_RESOURCES) when every player loads at once, and players booted in a
|
||||
* hidden tab size themselves at 0×0 (e.g. YouTube then keeps a blurry thumbnail).
|
||||
* Pages with many Loom embeds exhaust the browser's request budget
|
||||
* (ERR_INSUFFICIENT_RESOURCES) when every player loads at once, leaving some blank.
|
||||
*/
|
||||
function lazyLoadIframes(html: string): string {
|
||||
return html.replace(/<iframe\b(?![^>]*\bloading=)/gi, '<iframe loading="lazy"');
|
||||
}
|
||||
|
||||
function shouldLazyLoad(url: string): boolean {
|
||||
try {
|
||||
const { hostname } = new URL(url);
|
||||
return hostname === 'loom.com' || hostname.endsWith('.loom.com');
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an integration block with an unfurl action from the GitBook Embed response.
|
||||
*/
|
||||
|
||||
@@ -14,8 +14,6 @@ export function Details(props: {
|
||||
contentIds?: string[];
|
||||
open?: boolean;
|
||||
className?: ClassValue;
|
||||
/** Node key of the expandable, so comments can anchor to it. */
|
||||
commentKey?: string;
|
||||
}) {
|
||||
const { children, id, className } = props;
|
||||
|
||||
@@ -48,7 +46,6 @@ export function Details(props: {
|
||||
<details
|
||||
ref={ref}
|
||||
id={id}
|
||||
data-block-key={props.commentKey}
|
||||
open={props.open || openFromHash}
|
||||
className={tcls(
|
||||
className,
|
||||
|
||||
@@ -5,7 +5,6 @@ import type { BlockProps } from '../Block';
|
||||
import { Blocks } from '../Blocks';
|
||||
import { shouldShowHashLinks } from '../HashLinkButton';
|
||||
import { Inlines } from '../Inlines';
|
||||
import { getCommentAnchorProps, getCommentKeyProp } from '../utils';
|
||||
import { Details } from './Details';
|
||||
import { ToggleChevron } from '@/components/primitives';
|
||||
import { getSpaceLanguage, tString } from '@/intl/server';
|
||||
@@ -38,12 +37,10 @@ export async function Expandable(props: BlockProps<DocumentBlockExpandable>) {
|
||||
return (
|
||||
<Details
|
||||
id={id}
|
||||
{...getCommentKeyProp(context, block.key)}
|
||||
open={context.mode === 'print' || isDefaultExpanded}
|
||||
className={tcls('scroll-mt-(--content-scroll-margin)', style)}
|
||||
>
|
||||
<summary
|
||||
{...getCommentAnchorProps(context, titleParagraph.key)}
|
||||
className={tcls(
|
||||
'cursor-pointer',
|
||||
'px-4',
|
||||
|
||||
@@ -40,7 +40,7 @@ export async function File(props: BlockProps<DocumentBlockFile>) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Caption {...props} withBorder withBlockKey>
|
||||
<Caption {...props} withBorder>
|
||||
<div className="flex flex-wrap items-center gap-5 px-5 py-3">
|
||||
<div className="flex min-h-8 min-w-14 flex-col items-center justify-center gap-1 border-r border-tint-subtle pr-4">
|
||||
{contentType === 'image' ? (
|
||||
|
||||
@@ -5,7 +5,7 @@ import { HashLinkButton, hashLinkButtonWrapperStyles, shouldShowHashLinks } from
|
||||
import { HeadingRevealWrapper } from './HeadingRevealWrapper';
|
||||
import { Inlines } from './Inlines';
|
||||
import { getBlockTextStyle } from './spacing';
|
||||
import { getCommentAnchorProps, getTextAlignment } from './utils';
|
||||
import { getTextAlignment } from './utils';
|
||||
import { getSpaceLanguage, tString } from '@/intl/server';
|
||||
import { defaultLanguage } from '@/intl/translations';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
@@ -30,7 +30,6 @@ export async function Heading(props: BlockProps<DocumentBlockHeading>) {
|
||||
<HeadingRevealWrapper
|
||||
as={Tag}
|
||||
id={id}
|
||||
{...getCommentAnchorProps(context, block.key)}
|
||||
className={tcls(
|
||||
textStyle.textSize,
|
||||
'heading',
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { DocumentBlockImage, DocumentBlockImages, JSONDocument, Length } fr
|
||||
import type { BlockProps } from './Block';
|
||||
import { Caption, type CaptionAlign } from './Caption';
|
||||
import type { DocumentContext } from './DocumentView';
|
||||
import { getCommentAnchorProps } from './utils';
|
||||
import { Image, type ImageResponsiveSize } from '@/components/utils';
|
||||
import { resolveContentRefInDocument } from '@/lib/references';
|
||||
import { type ClassValue, tcls } from '@/lib/tailwind';
|
||||
@@ -16,7 +15,6 @@ export function Images(props: BlockProps<DocumentBlockImages>) {
|
||||
|
||||
return (
|
||||
<div
|
||||
{...getCommentAnchorProps(context, block.key)}
|
||||
className={tcls(
|
||||
style,
|
||||
'flex',
|
||||
|
||||
@@ -2,10 +2,9 @@ import type { DocumentBlockIntegration, RenderIntegrationUI } from '@gitbook/api
|
||||
import { ContentKitOutput } from '@gitbook/react-contentkit';
|
||||
|
||||
import type { BlockProps } from '../Block';
|
||||
import { getCommentAnchorProps, getCommentMarkerProps } from '../utils';
|
||||
import { getWebframePageContext } from './adaptive';
|
||||
import './contentkit.css';
|
||||
import { contentKitServerContext } from './contentkit';
|
||||
import './contentkit.css';
|
||||
import {
|
||||
ContentKitWithClientContext,
|
||||
type WebframeLinkerData,
|
||||
@@ -62,12 +61,7 @@ export async function IntegrationBlock(props: BlockProps<DocumentBlockIntegratio
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
// Integration output can render blocks with generated keys.
|
||||
{...getCommentAnchorProps(context, block.key)}
|
||||
{...getCommentMarkerProps(context, 'data-comment-block')}
|
||||
className={tcls(style)}
|
||||
>
|
||||
<div className={tcls(style)}>
|
||||
<pre>
|
||||
Unexpected error with integration {block.data.integration}:{' '}
|
||||
{initialResponse.error.message}
|
||||
@@ -105,12 +99,7 @@ export async function IntegrationBlock(props: BlockProps<DocumentBlockIntegratio
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
// Integration output can render blocks with generated keys.
|
||||
{...getCommentAnchorProps(context, block.key)}
|
||||
{...getCommentMarkerProps(context, 'data-comment-block')}
|
||||
className={tcls(style)}
|
||||
>
|
||||
<div className={tcls(style)}>
|
||||
<ContentKitWithClientContext
|
||||
{...contentKitProps}
|
||||
canAccessVisitorClaims={canAccessVisitorClaims}
|
||||
|
||||
@@ -11,13 +11,11 @@ import type {
|
||||
import type { BlockProps } from './Block';
|
||||
import { Blocks } from './Blocks';
|
||||
import { getBlockTextStyle } from './spacing';
|
||||
import { getCommentAnchorProps } from './utils';
|
||||
import { Checkbox } from '@/components/primitives';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
export function ListItem(props: BlockProps<DocumentBlockListItem>) {
|
||||
const { block, ancestorBlocks, ...contextProps } = props;
|
||||
const { context } = contextProps;
|
||||
|
||||
const parent = ancestorBlocks[ancestorBlocks.length - 1];
|
||||
assert(
|
||||
@@ -48,7 +46,7 @@ export function ListItem(props: BlockProps<DocumentBlockListItem>) {
|
||||
switch (parent.type) {
|
||||
case 'list-tasks':
|
||||
return (
|
||||
<ListItemLI block={block} anchorProps={getCommentAnchorProps(context, block.key)}>
|
||||
<ListItemLI block={block}>
|
||||
<ListItemPrefix block={block}>
|
||||
<Checkbox
|
||||
id={block.key!}
|
||||
@@ -66,7 +64,7 @@ export function ListItem(props: BlockProps<DocumentBlockListItem>) {
|
||||
);
|
||||
case 'list-ordered':
|
||||
return (
|
||||
<ListItemLI block={block} anchorProps={getCommentAnchorProps(context, block.key)}>
|
||||
<ListItemLI block={block}>
|
||||
<ListItemPrefix block={block}>
|
||||
<PseudoBefore
|
||||
content={getOrderedListItemPrefixContent({
|
||||
@@ -84,7 +82,7 @@ export function ListItem(props: BlockProps<DocumentBlockListItem>) {
|
||||
);
|
||||
case 'list-unordered':
|
||||
return (
|
||||
<ListItemLI block={block} anchorProps={getCommentAnchorProps(context, block.key)}>
|
||||
<ListItemLI block={block}>
|
||||
<ListItemPrefix block={block}>
|
||||
<PseudoBefore
|
||||
content={getUnorderedListItemsPrefixContent({
|
||||
@@ -125,17 +123,9 @@ function getListItemDepth(input: {
|
||||
return depth;
|
||||
}
|
||||
|
||||
function ListItemLI(props: {
|
||||
block: DocumentBlockListItem;
|
||||
anchorProps: ReturnType<typeof getCommentAnchorProps>;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
function ListItemLI(props: { block: DocumentBlockListItem; children: React.ReactNode }) {
|
||||
const textStyle = getBlockTextStyle(props.block);
|
||||
return (
|
||||
<li {...props.anchorProps} className={tcls(textStyle.lineHeight, 'flex items-start')}>
|
||||
{props.children}
|
||||
</li>
|
||||
);
|
||||
return <li className={tcls(textStyle.lineHeight, 'flex items-start')}>{props.children}</li>;
|
||||
}
|
||||
|
||||
function ListItemPrefix(props: { block: DocumentBlockListItem; children: React.ReactNode }) {
|
||||
|
||||
@@ -3,25 +3,21 @@ import { MathFormula } from '@gitbook/react-math';
|
||||
|
||||
import type { BlockProps } from './Block';
|
||||
import type { InlineProps } from './Inline';
|
||||
import { getCommentAnchorProps } from './utils';
|
||||
import { getAssetURL } from '@/lib/assets';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
const assetsUrl = getAssetURL('math');
|
||||
|
||||
export async function BlockMath(props: BlockProps<DocumentBlockMath>) {
|
||||
const { block, style, context } = props;
|
||||
const { block, style } = props;
|
||||
|
||||
// `MathFormula` only forwards `className`, so the comment anchor needs its own wrapper.
|
||||
return (
|
||||
<div {...getCommentAnchorProps(context, block.key)} className={tcls(style)}>
|
||||
<MathFormula
|
||||
formula={block.data.formula}
|
||||
inline={false}
|
||||
className="overflow-x-auto"
|
||||
assetsUrl={assetsUrl}
|
||||
/>
|
||||
</div>
|
||||
<MathFormula
|
||||
formula={block.data.formula}
|
||||
inline={false}
|
||||
className={tcls(style, 'overflow-x-auto')}
|
||||
assetsUrl={assetsUrl}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { BlockProps } from '../Block';
|
||||
import { getCommentAnchorProps, getCommentMarkerProps } from '../utils';
|
||||
import { getOpenAPIBlockClientProps } from './context';
|
||||
import { OpenAPIBlockLazy } from './OpenAPIBlockLazy';
|
||||
import { resolveOpenAPIOperationBlock } from '@/lib/openapi/resolveOpenAPIOperationBlock';
|
||||
@@ -10,14 +9,9 @@ import { tcls } from '@/lib/tailwind';
|
||||
* Render an openapi block or an openapi-operation block.
|
||||
*/
|
||||
export async function OpenAPIOperation(props: BlockProps<AnyOpenAPIOperationsBlock>) {
|
||||
const { style, block, context } = props;
|
||||
const { style } = props;
|
||||
return (
|
||||
<div
|
||||
{...getCommentAnchorProps(context, block.key)}
|
||||
// Its generated headings and descriptions carry keys that aren't page nodes.
|
||||
{...getCommentMarkerProps(context, 'data-comment-block')}
|
||||
className={tcls('flex w-full min-w-0', style, 'max-w-full')}
|
||||
>
|
||||
<div className={tcls('flex w-full min-w-0', style, 'max-w-full')}>
|
||||
<OpenAPIOperationBody {...props} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { BlockProps } from '../Block';
|
||||
import { getCommentAnchorProps, getCommentMarkerProps } from '../utils';
|
||||
import { getOpenAPIBlockClientProps } from './context';
|
||||
import { OpenAPIBlockLazy } from './OpenAPIBlockLazy';
|
||||
import { resolveOpenAPISchemasBlock } from '@/lib/openapi/resolveOpenAPISchemasBlock';
|
||||
@@ -10,14 +9,9 @@ import { tcls } from '@/lib/tailwind';
|
||||
* Render an openapi-schemas block.
|
||||
*/
|
||||
export async function OpenAPISchemas(props: BlockProps<OpenAPISchemasBlock>) {
|
||||
const { style, block, context } = props;
|
||||
const { style } = props;
|
||||
return (
|
||||
<div
|
||||
{...getCommentAnchorProps(context, block.key)}
|
||||
// Its generated headings and descriptions carry keys that aren't page nodes.
|
||||
{...getCommentMarkerProps(context, 'data-comment-block')}
|
||||
className={tcls('flex w-full', style, 'max-w-full')}
|
||||
>
|
||||
<div className={tcls('flex w-full', style, 'max-w-full')}>
|
||||
<OpenAPISchemasBody {...props} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { BlockProps } from '../Block';
|
||||
import { getCommentAnchorProps, getCommentMarkerProps } from '../utils';
|
||||
import { getOpenAPIBlockClientProps } from './context';
|
||||
import { OpenAPIBlockLazy } from './OpenAPIBlockLazy';
|
||||
import { resolveOpenAPIWebhookBlock } from '@/lib/openapi/resolveOpenAPIWebhookBlock';
|
||||
@@ -10,14 +9,9 @@ import { tcls } from '@/lib/tailwind';
|
||||
* Render an openapi block or an openapi-webhook block.
|
||||
*/
|
||||
export async function OpenAPIWebhook(props: BlockProps<OpenAPIWebhookBlock>) {
|
||||
const { style, block, context } = props;
|
||||
const { style } = props;
|
||||
return (
|
||||
<div
|
||||
{...getCommentAnchorProps(context, block.key)}
|
||||
// Its generated headings and descriptions carry keys that aren't page nodes.
|
||||
{...getCommentMarkerProps(context, 'data-comment-block')}
|
||||
className={tcls('flex w-full min-w-0', style, 'max-w-full')}
|
||||
>
|
||||
<div className={tcls('flex w-full min-w-0', style, 'max-w-full')}>
|
||||
<OpenAPIWebhookBody {...props} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,19 +4,15 @@ import { CustomizationAIMode } from '@gitbook/api';
|
||||
import type { BlockProps } from './Block';
|
||||
import { Inlines } from './Inlines';
|
||||
import { getTextAlignment } from './utils';
|
||||
import { getCommentAnchorProps } from './utils';
|
||||
import { getNodeText } from '@/lib/document';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
export function Paragraph(props: BlockProps<DocumentBlockParagraph>) {
|
||||
const { block, style, ancestorBlocks, ...contextProps } = props;
|
||||
const { context } = contextProps;
|
||||
// Like the app, a list item owns the comments on its direct paragraph.
|
||||
const isCommentTarget = ancestorBlocks.at(-1)?.type !== 'list-item';
|
||||
|
||||
const paragraph = (
|
||||
<p
|
||||
{...(isCommentTarget ? getCommentAnchorProps(context, block.key) : {})}
|
||||
// Cover-aware contrast text applies only to the page body, not to documents
|
||||
// rendered in overlays (search answers, AI chat) on a background-cover page.
|
||||
data-cover-aware-text={context.isPageBody ? '' : undefined}
|
||||
|
||||
@@ -9,14 +9,12 @@ import { validateIconName } from '@gitbook/icons/icons';
|
||||
import type { BlockProps } from '../Block';
|
||||
import { CodeBlock } from '../CodeBlock';
|
||||
import { getPlainCodeBlock } from '../CodeBlock/highlight-tokens';
|
||||
import { COMMENT_IGNORE_CLASSNAME, getCommentKeyProp } from '../utils';
|
||||
import { PromptClient } from './PromptClient';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
const PROMPT_COLLAPSED_LINE_COUNT = 10;
|
||||
|
||||
export function Prompt(props: BlockProps<DocumentBlockPrompt>) {
|
||||
const { block, document, ancestorBlocks, context, isEstimatedOffscreen, style } = props;
|
||||
const { block, document, ancestorBlocks, context, isEstimatedOffscreen } = props;
|
||||
const contentIcon =
|
||||
block.data.icon && validateIconName(block.data.icon) ? block.data.icon : null;
|
||||
const prompt = getPromptText(block);
|
||||
@@ -41,13 +39,6 @@ export function Prompt(props: BlockProps<DocumentBlockPrompt>) {
|
||||
defaultExpanded={defaultExpanded}
|
||||
openInAIProviders={getOpenInAIProviders(props)}
|
||||
prompt={prompt}
|
||||
{...getCommentKeyProp(context, block.key)}
|
||||
// Only the comment marker: the prompt keeps its own width rather than the block styles.
|
||||
className={
|
||||
tcls(style).split(' ').includes(COMMENT_IGNORE_CLASSNAME)
|
||||
? COMMENT_IGNORE_CLASSNAME
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{promptContent}
|
||||
</PromptClient>
|
||||
|
||||
@@ -13,9 +13,6 @@ import { tcls } from '@/lib/tailwind';
|
||||
type PromptClientProps = DocumentBlockPrompt['data'] & {
|
||||
contentIcon: IconName | null;
|
||||
prompt: string;
|
||||
/** Comment target: its code nodes aren't, so the prompt owns clicks inside it. */
|
||||
commentKey?: string;
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
@@ -26,8 +23,6 @@ export function PromptClient(props: PromptClientProps) {
|
||||
openInAIProviders = false,
|
||||
defaultExpanded = 'hidden',
|
||||
prompt,
|
||||
commentKey,
|
||||
className,
|
||||
children,
|
||||
} = props;
|
||||
const language = useLanguage();
|
||||
@@ -42,14 +37,11 @@ export function PromptClient(props: PromptClientProps) {
|
||||
|
||||
return (
|
||||
<div
|
||||
data-block-key={commentKey}
|
||||
data-comment-block={commentKey ? '' : undefined}
|
||||
className={tcls(
|
||||
'relative flex w-full flex-col overflow-hidden circular-corners:rounded-2xl rounded-corners:rounded-xl straight-corners:rounded-xs text-tint-strong',
|
||||
'border border-tint-subtle contrast-more:border-tint',
|
||||
'transition',
|
||||
expanded ? 'bg-tint depth-subtle:shadow-xs' : 'bg-tint-base',
|
||||
className
|
||||
expanded ? 'bg-tint depth-subtle:shadow-xs' : 'bg-tint-base'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { DocumentBlockReusableContent } from '@gitbook/api';
|
||||
|
||||
import type { BlockProps } from './Block';
|
||||
import { UnwrappedBlocks } from './Blocks';
|
||||
import { COMMENT_IGNORE_CLASSNAME } from './utils';
|
||||
import { getDataOrNull } from '@/lib/data';
|
||||
import { resolveContentRefInDocument } from '@/lib/references';
|
||||
|
||||
@@ -51,9 +50,6 @@ export async function ReusableContent(props: BlockProps<DocumentBlockReusableCon
|
||||
nodes={reusableContentDocument.nodes}
|
||||
document={reusableContentDocument}
|
||||
ancestorBlocks={[...ancestorBlocks, block]}
|
||||
// Its blocks carry keys of the reusable content's own document, which comments on the
|
||||
// page can't anchor to. A class, not a wrapper, which would break the page's spacing.
|
||||
blockStyle={context.withCommentAnchors ? COMMENT_IGNORE_CLASSNAME : undefined}
|
||||
context={{
|
||||
...context,
|
||||
contentContext: reusableContent.context,
|
||||
|
||||
@@ -4,12 +4,10 @@ import type { DocumentBlockStepperStep } from '@gitbook/api';
|
||||
|
||||
import type { BlockProps } from './Block';
|
||||
import { Blocks } from './Blocks';
|
||||
import { getCommentAnchorProps } from './utils';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
export function StepperStep(props: BlockProps<DocumentBlockStepperStep>) {
|
||||
const { block, style, ancestorBlocks, ...contextProps } = props;
|
||||
const { context } = contextProps;
|
||||
|
||||
const ancestor = ancestorBlocks[ancestorBlocks.length - 1];
|
||||
assert(ancestor?.type === 'stepper', 'Ancestor block must be a stepper');
|
||||
@@ -34,10 +32,7 @@ export function StepperStep(props: BlockProps<DocumentBlockStepperStep>) {
|
||||
})();
|
||||
|
||||
return (
|
||||
<div
|
||||
{...getCommentAnchorProps(context, block.key)}
|
||||
className={tcls('flex flex-row gap-4 md:gap-8', style)}
|
||||
>
|
||||
<div className={tcls('flex flex-row gap-4 md:gap-8', style)}>
|
||||
<div className="relative select-none">
|
||||
<div
|
||||
className={tcls(
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { getCommentCellProps } from '../utils';
|
||||
import { getTableCellMerge } from './cellMerges';
|
||||
import { getColumnWidth, getViewGridLayout, hasVisibleHeader } from './layout';
|
||||
import { RecordColumnValue } from './RecordColumnValue';
|
||||
@@ -6,7 +5,7 @@ import { StickyViewGrid } from './StickyViewGrid';
|
||||
import type { TableGridViewProps } from './Table';
|
||||
import { TableHoverTable } from './TableHoverTable';
|
||||
import { TableSearchTableBody } from './TableSearch';
|
||||
import { getColumnAlignment, getColumnVerticalAlignment, type VerticalAlignment } from './utils';
|
||||
import { type VerticalAlignment, getColumnAlignment, getColumnVerticalAlignment } from './utils';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
/** Semantic table renderer used when a cell spans multiple records. */
|
||||
@@ -142,7 +141,7 @@ function NativeViewGridBody(
|
||||
stickyFirstColumn: boolean;
|
||||
}
|
||||
) {
|
||||
const { block, view, records, withHeader, stickyFirstColumn, cellMergeLayout, context } = props;
|
||||
const { block, view, records, withHeader, stickyFirstColumn, cellMergeLayout } = props;
|
||||
const firstVisibleColumn = view.columns[0];
|
||||
const lastVisibleColumn = view.columns.at(-1);
|
||||
const recordsById = new Map(records.map((record) => [record[0], record] as const));
|
||||
@@ -220,7 +219,6 @@ function NativeViewGridBody(
|
||||
colSpan={colSpan}
|
||||
aria-rowspan={rowSpan}
|
||||
aria-colspan={colSpan}
|
||||
{...getCommentCellProps(context, recordId, column)}
|
||||
data-table-row-start={recordIndex}
|
||||
data-table-row-end={
|
||||
recordIndex + (cellMerge?.merge.rowSpan ?? 1) - 1
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { getCommentCellProps } from '../utils';
|
||||
import { getMergedCellWidth, getTableCellMerge } from './cellMerges';
|
||||
import { getColumnWidth } from './layout';
|
||||
import { RecordColumnValue } from './RecordColumnValue';
|
||||
@@ -55,7 +54,6 @@ export function RecordRow(
|
||||
<div
|
||||
key={column}
|
||||
role="cell"
|
||||
{...getCommentCellProps(context, record[0], column)}
|
||||
aria-colspan={cellMerge?.merge.colSpan}
|
||||
className={tcls(
|
||||
'relative flex flex-1 border-r px-3 py-2 align-middle text-sm last:border-r-0',
|
||||
|
||||
@@ -3,7 +3,7 @@ import assertNever from 'assert-never';
|
||||
import type { DocumentBlockTable, DocumentTableViewGrid } from '@gitbook/api';
|
||||
|
||||
import type { BlockProps } from '../Block';
|
||||
import { getCommentAnchorProps, getCommentMarkerProps, isBlockOffscreen } from '../utils';
|
||||
import { isBlockOffscreen } from '../utils';
|
||||
import { type TableCellMergeLayout, createTableCellMergeLayout } from './cellMerges';
|
||||
import { getViewGridLayout, hasVisibleHeader } from './layout';
|
||||
import { NativeViewGrid } from './NativeViewGrid';
|
||||
@@ -74,12 +74,7 @@ export function Table(props: BlockProps<DocumentBlockTable>) {
|
||||
block.data.view.type === 'grid' ? cellMergeLayout.recordGroups : undefined
|
||||
}
|
||||
>
|
||||
<div
|
||||
{...getCommentAnchorProps(context, block.key)}
|
||||
// Owns clicks inside, e.g. on blocks of a record's own document; cells still win.
|
||||
{...getCommentMarkerProps(context, 'data-comment-block')}
|
||||
className={tcls(style, 'flex flex-col gap-3')}
|
||||
>
|
||||
<div className={tcls(style, 'flex flex-col gap-3')}>
|
||||
{showSearch ? (
|
||||
<TableSearchInput
|
||||
selectColumns={selectColumns}
|
||||
|
||||
@@ -130,7 +130,7 @@ const TabPanel = memo(function TabPanel(props: {
|
||||
aria-labelledby={getTabButtonId(tab.id)}
|
||||
className="scroll-mt-[calc(var(--content-scroll-margin)+var(--spacing)*20)]"
|
||||
>
|
||||
<div className="px-8 py-6">{tab.body}</div>
|
||||
<div className="p-4">{tab.body}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -317,7 +317,7 @@ function TabButton(
|
||||
{...rest}
|
||||
type="button"
|
||||
className={tcls(
|
||||
'relative inline-flex max-w-full items-center gap-1.5 px-4 py-2.5 font-medium text-sm transition-[color]',
|
||||
'relative inline-flex max-w-full items-center gap-1.5 px-3.5 py-2 font-medium text-sm transition-[color]',
|
||||
props.className
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import type { DocumentContext } from '../DocumentView';
|
||||
|
||||
// Spread rather than set to `undefined`: an `undefined` prop is still written to the RSC payload,
|
||||
// so public pages would keep paying for the attribute names.
|
||||
|
||||
/** Marks blocks comments can't anchor to, passed down as their `style` (see `ReusableContent`). */
|
||||
export const COMMENT_IGNORE_CLASSNAME = 'comment-ignore';
|
||||
|
||||
/** The node key comments anchor to, only where commenting is possible. */
|
||||
export function getCommentAnchorProps(
|
||||
context: DocumentContext,
|
||||
key: string | undefined
|
||||
): { 'data-block-key'?: string } {
|
||||
return context.withCommentAnchors && key ? { 'data-block-key': key } : {};
|
||||
}
|
||||
|
||||
/** A table cell's record and column, only where commenting is possible. */
|
||||
export function getCommentCellProps(
|
||||
context: DocumentContext,
|
||||
record: string,
|
||||
column: string
|
||||
): { 'data-table-record'?: string; 'data-table-column'?: string } {
|
||||
return context.withCommentAnchors
|
||||
? { 'data-table-record': record, 'data-table-column': column }
|
||||
: {};
|
||||
}
|
||||
|
||||
/** A boolean comment marker, only where commenting is possible. */
|
||||
export function getCommentMarkerProps(
|
||||
context: DocumentContext,
|
||||
name: 'data-comment-block' | 'data-comment-ignore'
|
||||
): Partial<Record<typeof name, ''>> {
|
||||
return context.withCommentAnchors ? { [name]: '' } : {};
|
||||
}
|
||||
|
||||
/** The node key as a `commentKey` prop for components rendering it themselves (e.g. client ones). */
|
||||
export function getCommentKeyProp(
|
||||
context: DocumentContext,
|
||||
key: string | undefined
|
||||
): { commentKey?: string } {
|
||||
return context.withCommentAnchors && key ? { commentKey: key } : {};
|
||||
}
|
||||
|
||||
/** The anchor attribute for a `commentKey` prop, when set. */
|
||||
export function getCommentKeyAttributes(commentKey: string | undefined): {
|
||||
'data-block-key'?: string;
|
||||
} {
|
||||
return commentKey ? { 'data-block-key': commentKey } : {};
|
||||
}
|
||||
@@ -1,3 +1,2 @@
|
||||
export * from './isBlockOffscreen';
|
||||
export * from './textAlignment';
|
||||
export * from './commentAnchors';
|
||||
|
||||
@@ -10,7 +10,6 @@ import { Icon, type IconName, IconStyle } from '@gitbook/icons';
|
||||
|
||||
import { useAIChatController, useAIChatState } from '@/components/AI';
|
||||
import type { Assistant } from '@/components/AI';
|
||||
import { useTrackEvent } from '@/components/Insights';
|
||||
import { Button } from '@/components/primitives/Button';
|
||||
import { DropdownMenuItem, useDropdownMenuClose } from '@/components/primitives/DropdownMenu';
|
||||
import { getURLForLLM } from '@/components/utils';
|
||||
@@ -40,7 +39,6 @@ export function ActionOpenAssistant(props: {
|
||||
const { assistant, type, page } = props;
|
||||
const chat = useAIChatState();
|
||||
const chatController = useAIChatController();
|
||||
const trackEvent = useTrackEvent();
|
||||
const language = useLanguage();
|
||||
|
||||
return (
|
||||
@@ -57,7 +55,6 @@ export function ActionOpenAssistant(props: {
|
||||
)}
|
||||
disabled={chat.responding}
|
||||
onClick={() => {
|
||||
trackEvent({ type: 'ask_trigger_click', trigger: 'page' });
|
||||
// Stage a reference to the current page so the assistant is informed about
|
||||
// the context the user is asking from. Only the sidebar GitBook Assistant
|
||||
// uses the chat reference system.
|
||||
|
||||
@@ -58,9 +58,6 @@ export async function PageBody(props: {
|
||||
? hasTopLevelBlock(document, (block) => block.type === 'updates')
|
||||
: false;
|
||||
|
||||
// Comments can only be made on change request previews, so public pages skip their markers.
|
||||
const withCommentAnchors = Boolean(context.changeRequest);
|
||||
|
||||
// Render link previews only if there are less than LINK_PREVIEW_MAX_COUNT links in the document.
|
||||
const withLinkPreviews = document
|
||||
? !hasMoreThan(
|
||||
@@ -103,8 +100,6 @@ export async function PageBody(props: {
|
||||
/>
|
||||
) : null}
|
||||
<main
|
||||
// Comments on the whole page target anywhere in it that isn't a block.
|
||||
{...(withCommentAnchors ? { 'data-comment-page': '' } : {})}
|
||||
className={tcls(
|
||||
'relative min-w-0 flex-1',
|
||||
'break-anywhere', // Allow words to break if they are too long.
|
||||
@@ -155,7 +150,6 @@ export async function PageBody(props: {
|
||||
page,
|
||||
},
|
||||
withLinkPreviews,
|
||||
withCommentAnchors,
|
||||
isPageBody: true,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -255,8 +255,6 @@ export async function PageHeader(props: {
|
||||
<PageTags page={page} revision={revision} />
|
||||
{page.layout.title ? (
|
||||
<h1
|
||||
// Comments on the whole page get their pin next to the title.
|
||||
{...(context.changeRequest ? { 'data-comment-page-title': '' } : {})}
|
||||
data-cover-aware-text
|
||||
data-over-cover
|
||||
className={tcls(
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Icon, type IconName } from '@gitbook/icons';
|
||||
|
||||
import { Link, type LinkInsightsProps } from '.';
|
||||
import { ToggleChevron } from './ToggleChevron';
|
||||
import { Tooltip, type TooltipProps } from './Tooltip';
|
||||
import { Tooltip } from './Tooltip';
|
||||
import { type ClassValue, tcls } from '@/lib/tailwind';
|
||||
|
||||
export type DropdownButtonProps<E extends HTMLElement = HTMLElement> = Omit<
|
||||
@@ -41,8 +41,6 @@ export function DropdownMenu(props: {
|
||||
button: React.ReactElement<Record<string, unknown>>;
|
||||
/** Tooltip label for the button */
|
||||
buttonTooltip?: React.ReactNode;
|
||||
/** Placement of the button's tooltip. */
|
||||
buttonTooltipProps?: Pick<TooltipProps, 'side' | 'sideOffset' | 'anchor'>;
|
||||
/** Content of the dropdown */
|
||||
children: React.ReactNode;
|
||||
/** Custom styles */
|
||||
@@ -74,7 +72,6 @@ export function DropdownMenu(props: {
|
||||
const {
|
||||
button,
|
||||
buttonTooltip,
|
||||
buttonTooltipProps,
|
||||
children,
|
||||
className,
|
||||
openOnHover = false,
|
||||
@@ -101,7 +98,7 @@ export function DropdownMenu(props: {
|
||||
<DropdownMenuContext.Provider value={{ open, setOpen }}>
|
||||
<Menu.Root modal={false} open={open} onOpenChange={setOpen}>
|
||||
{buttonTooltip ? (
|
||||
<Tooltip label={buttonTooltip} disableHoverablePopup {...buttonTooltipProps}>
|
||||
<Tooltip label={buttonTooltip} disableHoverablePopup>
|
||||
{trigger}
|
||||
</Tooltip>
|
||||
) : (
|
||||
|
||||
@@ -10,8 +10,6 @@ export type TooltipProps = {
|
||||
side?: BaseTooltip.Positioner.Props['side'];
|
||||
align?: BaseTooltip.Positioner.Props['align'];
|
||||
sideOffset?: number;
|
||||
/** Positions against this element instead of the trigger, e.g. when the trigger is transformed. */
|
||||
anchor?: BaseTooltip.Positioner.Props['anchor'];
|
||||
delay?: number;
|
||||
disabled?: boolean;
|
||||
disableHoverablePopup?: boolean;
|
||||
@@ -33,7 +31,6 @@ export function Tooltip(
|
||||
side,
|
||||
align,
|
||||
sideOffset = 4,
|
||||
anchor,
|
||||
delay,
|
||||
disabled,
|
||||
disableHoverablePopup,
|
||||
@@ -72,7 +69,6 @@ export function Tooltip(
|
||||
side={side}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
anchor={anchor}
|
||||
collisionPadding={8}
|
||||
>
|
||||
<BaseTooltip.Popup
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { joinPathWithBaseURL } from '../paths';
|
||||
import { checkIsHttpURL } from '../urls';
|
||||
|
||||
/** Matches the `feedback` length accepted by `submitSiteAgentFeedback`. */
|
||||
@@ -14,7 +13,7 @@ export const agentFeedbackDescriptions = {
|
||||
goal: 'The broader end goal you were ultimately trying to accomplish (as/on behalf of the user) when you hit this issue. Gives the team the context you were working towards. Optional.',
|
||||
|
||||
pageURL: (siteURL: string | undefined) =>
|
||||
`The full URL of the page the issue is about${siteURL ? ` (e.g. ${joinPathWithBaseURL(siteURL, 'getting-started')})` : ''}, so the finding is linked to the exact page.`,
|
||||
`The full URL of the page the issue is about${siteURL ? ` (e.g. ${siteURL}/getting-started)` : ''}, so the finding is linked to the exact page.`,
|
||||
|
||||
pageReference:
|
||||
'The URL of the page the issue is about, so the finding is linked to the exact page. Omit it to report about the page the user is currently viewing.',
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
import { isAPITokenExpired } from './api-token';
|
||||
|
||||
function signToken(exp: number | undefined) {
|
||||
return jwt.sign(exp === undefined ? {} : { exp }, 'secret');
|
||||
}
|
||||
|
||||
describe('isAPITokenExpired', () => {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
it('returns false for a token valid for a while', () => {
|
||||
expect(isAPITokenExpired(signToken(now + 3600))).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true for an expired token', () => {
|
||||
expect(isAPITokenExpired(signToken(now - 60))).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for a token expiring within the margin', () => {
|
||||
expect(isAPITokenExpired(signToken(now + 60))).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for a token without an expiration', () => {
|
||||
expect(isAPITokenExpired(signToken(undefined))).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for a token that cannot be decoded', () => {
|
||||
expect(isAPITokenExpired('not-a-jwt')).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { jwtDecode } from 'jwt-decode';
|
||||
|
||||
import type { SiteAPIToken } from '@gitbook/api';
|
||||
|
||||
/**
|
||||
* Refresh slightly ahead of the real expiry so the token survives the rest of the request.
|
||||
* The API mints tokens on a UTC-midnight bucket that stay valid until 00:05 the next day, so this
|
||||
* margin must stay under that 5min overlap: outside it, a refresh returns the very same token.
|
||||
*/
|
||||
const EXPIRATION_MARGIN_SECONDS = 120;
|
||||
|
||||
/**
|
||||
* Check if a site API token is expired, or about to expire.
|
||||
*/
|
||||
export function isAPITokenExpired(apiToken: string): boolean {
|
||||
try {
|
||||
const decoded = jwtDecode<SiteAPIToken & { exp?: number }>(apiToken);
|
||||
return (
|
||||
typeof decoded.exp === 'number' &&
|
||||
decoded.exp < Date.now() / 1000 + EXPIRATION_MARGIN_SECONDS
|
||||
);
|
||||
} catch {
|
||||
// A token we cannot decode is not one we can refresh.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
|
||||
import { race } from './async';
|
||||
import { flushWaitUntil } from './waitUntil';
|
||||
|
||||
describe('race', () => {
|
||||
it('should return on the first result and delay the others', async () => {
|
||||
const result = await race(
|
||||
[
|
||||
[2, 20],
|
||||
[1, 10],
|
||||
[3, 30],
|
||||
],
|
||||
async ([value, timeout]) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, timeout));
|
||||
return value;
|
||||
}
|
||||
);
|
||||
|
||||
expect(result).toBe(1);
|
||||
expect(await flushWaitUntil()).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should ignore null resolutions', async () => {
|
||||
const result = await race(
|
||||
[
|
||||
[2, 20],
|
||||
[null, 10],
|
||||
[3, 30],
|
||||
] as const,
|
||||
async ([value, timeout]) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, timeout));
|
||||
return value;
|
||||
}
|
||||
);
|
||||
|
||||
expect(result).toBe(2);
|
||||
expect(await flushWaitUntil()).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should ignore errors', async () => {
|
||||
const result = await race(
|
||||
[
|
||||
[new Error('test'), 20],
|
||||
[null, 10],
|
||||
[3, 30],
|
||||
] as const,
|
||||
async ([value, timeout]) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, timeout));
|
||||
|
||||
if (value instanceof Error) {
|
||||
throw value;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
);
|
||||
|
||||
expect(result).toBe(3);
|
||||
expect(await flushWaitUntil()).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should pass an abort signal to each input', async () => {
|
||||
const result = await race(
|
||||
[
|
||||
[2, 20],
|
||||
[1, 10],
|
||||
[3, 30],
|
||||
],
|
||||
async ([value, timeout], { signal }) => {
|
||||
await new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(resolve, timeout);
|
||||
|
||||
signal.addEventListener('abort', () => {
|
||||
clearTimeout(timeoutId);
|
||||
reject(new Error('Aborted'));
|
||||
});
|
||||
});
|
||||
return value;
|
||||
}
|
||||
);
|
||||
|
||||
expect(result).toBe(1);
|
||||
expect(await flushWaitUntil()).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should respect timeout', async () => {
|
||||
const result = await race(
|
||||
[
|
||||
[1, 10],
|
||||
[2, 20],
|
||||
[3, 30],
|
||||
],
|
||||
async ([value, timeout]) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, timeout));
|
||||
return value;
|
||||
},
|
||||
{
|
||||
timeout: 5,
|
||||
}
|
||||
);
|
||||
|
||||
expect(result).toBe(null);
|
||||
expect(await flushWaitUntil()).toHaveLength(3);
|
||||
});
|
||||
|
||||
describe('signal', () => {
|
||||
it('should abort all inputs', async () => {
|
||||
let abortted = 0;
|
||||
|
||||
const abort = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abort.abort();
|
||||
}, 5);
|
||||
|
||||
await expect(
|
||||
race(
|
||||
[
|
||||
[2, 20],
|
||||
[1, 10],
|
||||
[3, 30],
|
||||
],
|
||||
async ([value, timeout], { signal }) => {
|
||||
await new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(resolve, timeout);
|
||||
|
||||
signal.addEventListener('abort', () => {
|
||||
abortted += 1;
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
reject(new Error('Aborted'));
|
||||
});
|
||||
});
|
||||
return value;
|
||||
},
|
||||
{
|
||||
signal: abort.signal,
|
||||
}
|
||||
)
|
||||
).rejects.toThrow('The operation was aborted.');
|
||||
|
||||
expect(abortted).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('blockTimeout', () => {
|
||||
it('should fallback if blockTimeout is reached', async () => {
|
||||
const result = await race(
|
||||
[
|
||||
[null, 10],
|
||||
[2, 200],
|
||||
[1, 100],
|
||||
[3, 300],
|
||||
] as const,
|
||||
async ([value, timeout], { signal }) => {
|
||||
await new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(resolve, timeout);
|
||||
|
||||
signal.addEventListener('abort', () => {
|
||||
clearTimeout(timeoutId);
|
||||
reject(new Error('Aborted'));
|
||||
});
|
||||
});
|
||||
return value;
|
||||
},
|
||||
{
|
||||
blockTimeout: 20,
|
||||
blockFallback: async () => {
|
||||
return 4;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
expect(result).toBe(4);
|
||||
expect(await flushWaitUntil()).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should resolve only if fallback if blockTimeout is reached', async () => {
|
||||
const result = await race(
|
||||
[
|
||||
[null, 10],
|
||||
[2, 15],
|
||||
[1, 100],
|
||||
[3, 300],
|
||||
] as const,
|
||||
async ([value, timeout], { signal }) => {
|
||||
await new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(resolve, timeout);
|
||||
|
||||
signal.addEventListener('abort', () => {
|
||||
clearTimeout(timeoutId);
|
||||
reject(new Error('Aborted'));
|
||||
});
|
||||
});
|
||||
return value;
|
||||
},
|
||||
{
|
||||
blockTimeout: 20,
|
||||
blockFallback: async () => {
|
||||
return 4;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
expect(result).toBe(2);
|
||||
expect(await flushWaitUntil()).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should resolve with fallback if all inputs resolved to null', async () => {
|
||||
const result = await race(
|
||||
[
|
||||
[null, 20],
|
||||
[null, 25],
|
||||
[null, 30],
|
||||
] as const,
|
||||
async ([value, timeout], { signal }) => {
|
||||
await new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(resolve, timeout);
|
||||
|
||||
signal.addEventListener('abort', () => {
|
||||
clearTimeout(timeoutId);
|
||||
reject(new Error('Aborted'));
|
||||
});
|
||||
});
|
||||
return value;
|
||||
},
|
||||
{
|
||||
blockTimeout: 15,
|
||||
blockFallback: async () => {
|
||||
// Wait for all inputs to resolve
|
||||
await new Promise((resolve) => setTimeout(resolve, 30));
|
||||
return 4;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
expect(result).toBe(4);
|
||||
|
||||
const pendings = await flushWaitUntil();
|
||||
expect(pendings).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should throw error from fallback if fallbackOnNull is set to true after inputs finished', async () => {
|
||||
await expect(
|
||||
race(
|
||||
[
|
||||
[null, 3],
|
||||
[null, 4],
|
||||
[null, 5],
|
||||
] as const,
|
||||
async ([value, timeout], { signal }) => {
|
||||
await new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(resolve, timeout);
|
||||
|
||||
signal.addEventListener('abort', () => {
|
||||
clearTimeout(timeoutId);
|
||||
reject(new Error('Aborted'));
|
||||
});
|
||||
});
|
||||
|
||||
return value;
|
||||
},
|
||||
{
|
||||
blockTimeout: 15,
|
||||
blockFallback: async () => {
|
||||
// Wait for all inputs to resolve
|
||||
throw new Error('Fallback error');
|
||||
},
|
||||
fallbackOnNull: true,
|
||||
}
|
||||
)
|
||||
).rejects.toThrow('Fallback error');
|
||||
|
||||
const pendings = await flushWaitUntil();
|
||||
expect(pendings).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should throw error from fallback if fallbackOnNull is set to true before inputs finished with null', async () => {
|
||||
await expect(
|
||||
race(
|
||||
[
|
||||
[null, 30],
|
||||
[null, 40],
|
||||
[null, 50],
|
||||
] as const,
|
||||
async ([value, timeout], { signal }) => {
|
||||
await new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(resolve, timeout);
|
||||
|
||||
signal.addEventListener('abort', () => {
|
||||
clearTimeout(timeoutId);
|
||||
reject(new Error('Aborted'));
|
||||
});
|
||||
});
|
||||
|
||||
return value;
|
||||
},
|
||||
{
|
||||
blockTimeout: 5,
|
||||
blockFallback: async () => {
|
||||
throw new Error('Fallback error');
|
||||
},
|
||||
fallbackOnNull: true,
|
||||
}
|
||||
)
|
||||
).rejects.toThrow('Fallback error');
|
||||
|
||||
const pendings = await flushWaitUntil();
|
||||
expect(pendings).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not throw error from fallback if fallbackOnNull is set to true before inputs finished with non-null null', async () => {
|
||||
const result = await race(
|
||||
[
|
||||
[null, 10],
|
||||
['done', 15],
|
||||
[null, 20],
|
||||
] as const,
|
||||
async ([value, timeout], { signal }) => {
|
||||
await new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(resolve, timeout);
|
||||
|
||||
signal.addEventListener('abort', () => {
|
||||
clearTimeout(timeoutId);
|
||||
reject(new Error('Aborted'));
|
||||
});
|
||||
});
|
||||
|
||||
return value;
|
||||
},
|
||||
{
|
||||
blockTimeout: 5,
|
||||
blockFallback: async () => {
|
||||
throw new Error('Fallback error');
|
||||
},
|
||||
fallbackOnNull: true,
|
||||
}
|
||||
);
|
||||
expect(result).toBe('done');
|
||||
|
||||
const pendings = await flushWaitUntil();
|
||||
expect(pendings).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should throw the error if the blockFallback fails after the inputs with fallbackOnNull=false', async () => {
|
||||
await expect(
|
||||
race(
|
||||
[
|
||||
[null, 10],
|
||||
[null, 15],
|
||||
[null, 20],
|
||||
] as const,
|
||||
async ([value, timeout], { signal }) => {
|
||||
await new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(resolve, timeout);
|
||||
|
||||
signal.addEventListener('abort', () => {
|
||||
clearTimeout(timeoutId);
|
||||
reject(new Error('Aborted'));
|
||||
});
|
||||
});
|
||||
|
||||
return value;
|
||||
},
|
||||
{
|
||||
blockTimeout: 5,
|
||||
blockFallback: async () => {
|
||||
// Sleep 30ms to make sure all inputs are resolved
|
||||
await new Promise((resolve) => setTimeout(resolve, 30));
|
||||
throw new Error('Fallback error');
|
||||
},
|
||||
fallbackOnNull: false,
|
||||
}
|
||||
)
|
||||
).rejects.toThrow('Fallback error');
|
||||
|
||||
const pendings = await flushWaitUntil();
|
||||
expect(pendings).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,222 @@
|
||||
import { waitUntil } from './waitUntil';
|
||||
|
||||
/**
|
||||
* Execute a function for each input in parallel and return the first result.
|
||||
* It is designed to support executing in a worker environment where async execution outside the request lifecycle is not allowed.
|
||||
* And also to support promise cancellation and optional result.
|
||||
*
|
||||
* Pattern 1: it waits for the first input to resolve with a non-null result.
|
||||
* I1: |-----------------| (r1)
|
||||
* I2: |---| (null)
|
||||
* I3: |-------| (r3) <- The function will return r3.
|
||||
*
|
||||
* Pattern 2: it returns `null` if the timeout is reached.
|
||||
* T: |------------| (timeout) <- The function will return `null`.
|
||||
* I1: |-----------------| (r1)
|
||||
* I2: |--------------| (null)
|
||||
* I3: |------------------| (r3)
|
||||
*
|
||||
* Pattern 3: it sends a signal to stop pending operations as soon as one resolves.
|
||||
* I1: |-------x cancelled
|
||||
* I2: |---| (null)
|
||||
* I3: |-------| (r3) <- The function will return r3.
|
||||
*
|
||||
* Pattern 4: it starts the blockFallback function if the blockTimeout is reached before any input resolves.
|
||||
* BT: |-----------------| (blockTimeout)
|
||||
* F: |---| (fallback) <- The function will return the result of the fallback.
|
||||
* I1: |---------------------x cancelled
|
||||
* I2: |---------------------x cancelled
|
||||
* I3: |---------------------x cancelled
|
||||
*
|
||||
* Pattern 5: it resolves with the first resolution even after fallback was started
|
||||
* BT: |-----------------| (blockTimeout)
|
||||
* F: |---x fallback cancelled
|
||||
* I1: |---------------------| (r1) <- The function will return r1.
|
||||
* I2: |-------------------| (null)
|
||||
* I3: |---------------------x cancelled
|
||||
*/
|
||||
export async function race<I, R>(
|
||||
inputs: I[],
|
||||
execute: (input: I, options: { signal: AbortSignal }) => Promise<R | null>,
|
||||
options: {
|
||||
/**
|
||||
* Signal to cancel all operations.
|
||||
*/
|
||||
signal?: AbortSignal;
|
||||
|
||||
/**
|
||||
* Timeout in milliseconds before all operations are cancelled.
|
||||
*/
|
||||
timeout?: number;
|
||||
|
||||
/**
|
||||
* Timeout in milliseconds.
|
||||
* if the timeout is reached, the function will start calling the `blockFallback` function.
|
||||
*/
|
||||
blockTimeout?: number;
|
||||
|
||||
/**
|
||||
* Fallback to call when the timeout is reached.
|
||||
* If an input resolve before the timeout, the fallback will be ignored.
|
||||
* If an input resolve while the fallback is running, the fallback will be cancelled.
|
||||
*/
|
||||
blockFallback?: (options: { signal: AbortSignal }) => Promise<R | null>;
|
||||
|
||||
/**
|
||||
* If true, the `blockFallback` will be called if all inputs resolved with `null`.
|
||||
* @default false
|
||||
*/
|
||||
fallbackOnNull?: boolean;
|
||||
} = {}
|
||||
): Promise<R | null> {
|
||||
const { signal, timeout, blockTimeout, blockFallback, fallbackOnNull = false } = options;
|
||||
const result = await new Promise<R | null>((resolve, reject) => {
|
||||
let resolved = false;
|
||||
let pending = inputs.length;
|
||||
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
let blockFallbackStarted = false;
|
||||
let blockFallbackRunning = false;
|
||||
let blockFallbackError: Error | null = null;
|
||||
let blockTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
const abort = new AbortController();
|
||||
|
||||
const done = () => {
|
||||
resolved = true;
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
if (blockTimeoutId) {
|
||||
clearTimeout(blockTimeoutId);
|
||||
}
|
||||
abort.abort();
|
||||
};
|
||||
|
||||
const rejectWith = (error: Error) => {
|
||||
if (!resolved) {
|
||||
done();
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
const resolveWith = (value: R | null) => {
|
||||
if (!resolved) {
|
||||
done();
|
||||
resolve(value);
|
||||
}
|
||||
};
|
||||
|
||||
if (signal) {
|
||||
// If top function is cancelling, we should cancel all pending operations
|
||||
signal.addEventListener('abort', () => {
|
||||
resolveWith(null);
|
||||
});
|
||||
}
|
||||
|
||||
const runFallback = () => {
|
||||
if (!blockFallback) {
|
||||
throw new Error('blockFallback is required');
|
||||
}
|
||||
|
||||
if (blockFallbackRunning || blockFallbackStarted) {
|
||||
throw new Error('blockFallback already started');
|
||||
}
|
||||
|
||||
if (blockTimeoutId) {
|
||||
clearTimeout(blockTimeoutId);
|
||||
}
|
||||
|
||||
blockFallbackStarted = true;
|
||||
blockFallbackRunning = true;
|
||||
waitUntil(
|
||||
blockFallback({
|
||||
signal: abort.signal,
|
||||
}).then(
|
||||
(fallbackResult) => {
|
||||
resolveWith(fallbackResult);
|
||||
},
|
||||
(error) => {
|
||||
logIgnoredError('blockFallback failed with', error);
|
||||
|
||||
if (pending === 0) {
|
||||
rejectWith(error);
|
||||
} else {
|
||||
blockFallbackError = error;
|
||||
blockFallbackRunning = false;
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
if (timeout) {
|
||||
timeoutId = setTimeout(() => {
|
||||
resolveWith(null);
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
if (blockTimeout) {
|
||||
if (!blockFallback) {
|
||||
throw new Error('blockTimeout requires blockFallback');
|
||||
}
|
||||
|
||||
blockTimeoutId = setTimeout(runFallback, blockTimeout);
|
||||
}
|
||||
|
||||
inputs.forEach((input, inputIndex) => {
|
||||
waitUntil(
|
||||
execute(input, { signal: abort.signal })
|
||||
.then(
|
||||
(inputResult) => {
|
||||
if (abort.signal.aborted) {
|
||||
// Ignore input results if the blockFallback has started
|
||||
return;
|
||||
}
|
||||
|
||||
if (inputResult !== null) {
|
||||
// Only resolve if we actually got a non-null result
|
||||
resolveWith(inputResult);
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
// Ignore errors
|
||||
logIgnoredError(`input ${inputIndex} failed with`, error);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
pending -= 1;
|
||||
if (pending === 0 && !resolved && !blockFallbackRunning) {
|
||||
if (fallbackOnNull && !blockFallbackStarted) {
|
||||
runFallback();
|
||||
} else {
|
||||
if (blockFallbackError) {
|
||||
rejectWith(blockFallbackError);
|
||||
} else {
|
||||
resolveWith(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Since the cancellation of the signal will cause the promise to resolve to `null`,
|
||||
// we need to throw an error to indicate that the operation was cancelled.
|
||||
signal?.throwIfAborted();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log and ignore an error.
|
||||
* It skips the error if it's an AbortError.
|
||||
*/
|
||||
function logIgnoredError(_message: string, error: Error) {
|
||||
if (error.name === 'AbortError' || process.env.NODE_ENV === 'test') {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try catch a promise and return the result or the error.
|
||||
*/
|
||||
|
||||
@@ -4,7 +4,6 @@ import type { PublishedSiteContentLookup } from '@gitbook/api';
|
||||
|
||||
import { GITBOOK_PREVIEW_BASE_URL } from '../env';
|
||||
import * as api from './api';
|
||||
import { DataFetcherError } from './errors';
|
||||
import { lookupPublishedContentByUrl } from './lookup';
|
||||
|
||||
describe('preview auth redirects', () => {
|
||||
@@ -107,55 +106,6 @@ describe('preview auth redirects', () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe('lookup URL', () => {
|
||||
it('resolves the full URL without search params in a single call', async () => {
|
||||
const urls: string[] = [];
|
||||
const apiClientSpy = spyOn(api, 'apiClient').mockReturnValue({
|
||||
urls: {
|
||||
async resolvePublishedContentByUrl({ url }: { url: string }) {
|
||||
urls.push(url);
|
||||
return { data: { target: 'external', redirect: 'https://example.com' } };
|
||||
},
|
||||
},
|
||||
} as unknown as ReturnType<typeof api.apiClient>);
|
||||
|
||||
try {
|
||||
await lookupPublishedContentByUrl({
|
||||
url: 'https://docs.example.com/v/variant/a/b/c/d/e?theme=dark',
|
||||
apiToken: null,
|
||||
redirectOnError: false,
|
||||
visitorPayload: {},
|
||||
});
|
||||
} finally {
|
||||
apiClientSpy.mockRestore();
|
||||
}
|
||||
|
||||
expect(urls).toEqual(['https://docs.example.com/v/variant/a/b/c/d/e']);
|
||||
});
|
||||
|
||||
it('returns the API error', async () => {
|
||||
const apiClientSpy = spyOn(api, 'apiClient').mockReturnValue({
|
||||
urls: {
|
||||
async resolvePublishedContentByUrl() {
|
||||
throw new DataFetcherError('Not found', 404);
|
||||
},
|
||||
},
|
||||
} as unknown as ReturnType<typeof api.apiClient>);
|
||||
|
||||
try {
|
||||
const result = await lookupPublishedContentByUrl({
|
||||
url: 'https://docs.example.com/a',
|
||||
apiToken: null,
|
||||
redirectOnError: false,
|
||||
visitorPayload: {},
|
||||
});
|
||||
expect(result.error?.code).toBe(404);
|
||||
} finally {
|
||||
apiClientSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('visitor payload', () => {
|
||||
it('forwards the visitor country to the API', async () => {
|
||||
const calls: { visitor?: unknown }[] = [];
|
||||
|
||||
@@ -3,8 +3,10 @@ import type { GitBookAPI, PublishedSiteContentLookup, SiteVisitorPayload } from
|
||||
import { apiClient } from './api';
|
||||
import { getExposableError } from './errors';
|
||||
import type { DataFetcherResponse } from './types';
|
||||
import { getURLLookup, getURLLookupPathname, stripURLSearch } from './urls';
|
||||
import { tryCatch } from '@/lib/async';
|
||||
import { getURLLookupAlternatives, getURLLookupPathname, stripURLSearch } from './urls';
|
||||
import { isAPITokenExpired } from '@/lib/api-token';
|
||||
import { race, tryCatch } from '@/lib/async';
|
||||
import { getLogger } from '@/lib/logger';
|
||||
import { joinPath, joinPathWithBaseURL } from '@/lib/paths';
|
||||
import { isPreviewRequest } from '@/lib/preview';
|
||||
import { trace } from '@/lib/tracing';
|
||||
@@ -20,14 +22,137 @@ interface LookupPublishedContentByUrlInput {
|
||||
|
||||
/**
|
||||
* Lookup a content by its URL using the GitBook resolvePublishedContentByUrl API endpoint.
|
||||
* To optimize caching, we try multiple lookup alternatives and return the first one that matches.
|
||||
*/
|
||||
export async function lookupPublishedContentByUrl(
|
||||
input: LookupPublishedContentByUrlInput
|
||||
): Promise<DataFetcherResponse<PublishedSiteContentLookup>> {
|
||||
const lookupURL = new URL(input.url);
|
||||
const lookup = getURLLookup(stripURLSearch(lookupURL));
|
||||
const url = stripURLSearch(lookupURL);
|
||||
const lookup = getURLLookupAlternatives(url);
|
||||
|
||||
if (!lookup) {
|
||||
const result = await race(lookup.urls, async (alternative, { signal }) => {
|
||||
const api = apiClient({ apiToken: input.apiToken });
|
||||
const resolveURL = (cacheBust?: string) =>
|
||||
tryCatch(
|
||||
api.urls.resolvePublishedContentByUrl(
|
||||
{
|
||||
url: alternative.url,
|
||||
...(input.visitorPayload ? { visitor: input.visitorPayload } : {}),
|
||||
redirectOnError: input.redirectOnError,
|
||||
// Temporary: the API caches this POST by request body, so an unknown
|
||||
// field is enough to miss the cache and get a freshly minted token.
|
||||
...(cacheBust ? { cacheBust } : {}),
|
||||
} as ResolveBody, //TODO: remove cast when we are sure that everything is good
|
||||
{ signal }
|
||||
)
|
||||
);
|
||||
|
||||
let callResult = await trace(
|
||||
{
|
||||
operation: 'resolvePublishedContentByUrl',
|
||||
name: alternative.url,
|
||||
},
|
||||
() => resolveURL()
|
||||
);
|
||||
|
||||
const resolved = callResult.error ? null : callResult.data.data;
|
||||
if (
|
||||
resolved &&
|
||||
!('redirect' in resolved) &&
|
||||
// Only alternatives we'd actually accept are worth a second round-trip.
|
||||
(alternative.primary || resolved.complete) &&
|
||||
isAPITokenExpired(resolved.apiToken)
|
||||
) {
|
||||
getLogger().warn(
|
||||
'resolvePublishedContentByUrl returned an expired API token, retrying without cache'
|
||||
);
|
||||
callResult = await trace(
|
||||
{
|
||||
operation: 'resolvePublishedContentByUrlUncached',
|
||||
name: alternative.url,
|
||||
},
|
||||
// The value only needs to differ between calls, so `Math.random` is enough.
|
||||
() => resolveURL(Math.random().toString(36).slice(2))
|
||||
);
|
||||
}
|
||||
|
||||
if (callResult.error) {
|
||||
if (alternative.primary) {
|
||||
// We only return an error for the primary alternative (full URL),
|
||||
// as other parts could result in errors due to the URL being incomplete (share links, etc).
|
||||
return { error: callResult.error };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
data: { data },
|
||||
} = callResult;
|
||||
|
||||
if ('redirect' in data) {
|
||||
if (alternative.primary) {
|
||||
if (data.target === 'application' && isPreviewRequest(lookupURL)) {
|
||||
// The cached lookup omits content selectors and query params needed after login.
|
||||
const redirect = new URL(data.redirect);
|
||||
redirect.searchParams.set('redirect', lookupURL.toString());
|
||||
return { data: { ...data, redirect: redirect.toString() } };
|
||||
}
|
||||
|
||||
// Append the path to the redirect URL
|
||||
// because we might have matched a shorter path and the redirect is relative to it
|
||||
if (alternative.extraPath) {
|
||||
if (data.target === 'content') {
|
||||
const redirect = new URL(data.redirect);
|
||||
redirect.pathname = joinPath(redirect.pathname, alternative.extraPath);
|
||||
data.redirect = redirect.toString();
|
||||
} else {
|
||||
const redirect = new URL(data.redirect);
|
||||
if (redirect.searchParams.has('location')) {
|
||||
redirect.searchParams.set(
|
||||
'location',
|
||||
joinPath(
|
||||
redirect.searchParams.get('location') ?? '',
|
||||
alternative.extraPath
|
||||
)
|
||||
);
|
||||
data.redirect = redirect.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { data };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* We use the following criteria to determine if the lookup result is the right one:
|
||||
* - the primary alternative was resolved (because that's the longest or most inclusive path)
|
||||
* - the resolution of the site URL is complete (because we want to resolve the deepest path possible)
|
||||
*
|
||||
* In both cases, the idea is to use the deepest/longest/most inclusive path to resolve the content.
|
||||
*/
|
||||
if (alternative.primary || ('site' in data && data.complete)) {
|
||||
const changeRequest = data.changeRequest ?? lookup.changeRequest;
|
||||
const revision = data.revision ?? lookup.revision;
|
||||
|
||||
const siteResult: PublishedSiteContentLookup = {
|
||||
...data,
|
||||
canonicalUrl: joinPathWithBaseURL(data.canonicalUrl, alternative.extraPath),
|
||||
basePath: joinPath(data.basePath, lookup.basePath ?? ''),
|
||||
pathname: getURLLookupPathname(alternative, data),
|
||||
...(changeRequest ? { changeRequest } : {}),
|
||||
...(revision ? { revision } : {}),
|
||||
};
|
||||
return { data: siteResult };
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return {
|
||||
error: {
|
||||
code: 404,
|
||||
@@ -36,73 +161,13 @@ export async function lookupPublishedContentByUrl(
|
||||
};
|
||||
}
|
||||
|
||||
const api = apiClient({ apiToken: input.apiToken });
|
||||
const callResult = await trace(
|
||||
{
|
||||
operation: 'resolvePublishedContentByUrl',
|
||||
name: lookup.url,
|
||||
},
|
||||
() =>
|
||||
tryCatch(
|
||||
api.urls.resolvePublishedContentByUrl({
|
||||
url: lookup.url,
|
||||
...(input.visitorPayload ? { visitor: input.visitorPayload } : {}),
|
||||
redirectOnError: input.redirectOnError,
|
||||
} as ResolveBody) //TODO: remove cast when we are sure that everything is good
|
||||
)
|
||||
);
|
||||
|
||||
if (callResult.error) {
|
||||
if (result.error) {
|
||||
return {
|
||||
error: getExposableError(callResult.error),
|
||||
error: getExposableError(result.error),
|
||||
};
|
||||
}
|
||||
|
||||
const {
|
||||
data: { data },
|
||||
} = callResult;
|
||||
|
||||
if ('redirect' in data) {
|
||||
if (data.target === 'application' && isPreviewRequest(lookupURL)) {
|
||||
// The cached lookup omits content selectors and query params needed after login.
|
||||
const redirect = new URL(data.redirect);
|
||||
redirect.searchParams.set('redirect', lookupURL.toString());
|
||||
return { data: { ...data, redirect: redirect.toString() } };
|
||||
}
|
||||
|
||||
// Append the path to the redirect URL
|
||||
// because we resolved a shorter path and the redirect is relative to it
|
||||
if (lookup.extraPath) {
|
||||
if (data.target === 'content') {
|
||||
const redirect = new URL(data.redirect);
|
||||
redirect.pathname = joinPath(redirect.pathname, lookup.extraPath);
|
||||
data.redirect = redirect.toString();
|
||||
} else {
|
||||
const redirect = new URL(data.redirect);
|
||||
if (redirect.searchParams.has('location')) {
|
||||
redirect.searchParams.set(
|
||||
'location',
|
||||
joinPath(redirect.searchParams.get('location') ?? '', lookup.extraPath)
|
||||
);
|
||||
data.redirect = redirect.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { data };
|
||||
}
|
||||
|
||||
const changeRequest = data.changeRequest ?? lookup.changeRequest;
|
||||
const revision = data.revision ?? lookup.revision;
|
||||
|
||||
return {
|
||||
data: {
|
||||
...data,
|
||||
canonicalUrl: joinPathWithBaseURL(data.canonicalUrl, lookup.extraPath),
|
||||
basePath: joinPath(data.basePath, lookup.basePath ?? ''),
|
||||
pathname: getURLLookupPathname(lookup, data),
|
||||
...(changeRequest ? { changeRequest } : {}),
|
||||
...(revision ? { revision } : {}),
|
||||
},
|
||||
data: result.data,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
|
||||
import { getURLLookup, getURLLookupPathname, normalizeURL } from './urls';
|
||||
import {
|
||||
getURLLookupAlternatives,
|
||||
getURLLookupPathname,
|
||||
normalizeURL,
|
||||
shouldBypassLookupAlternatives,
|
||||
} from './urls';
|
||||
|
||||
describe('getURLLookupPathname', () => {
|
||||
const previewRoot = 'https://sites.gitbook.com/preview/site_example/section';
|
||||
@@ -10,12 +15,13 @@ describe('getURLLookupPathname', () => {
|
||||
it.each(['revisions', 'changes'])(
|
||||
'resolves a page in a %s preview from the site-space root',
|
||||
(kind) => {
|
||||
const lookup = getURLLookup(
|
||||
const lookup = getURLLookupAlternatives(
|
||||
new URL(`${previewRoot}/~/${kind}/revision-id/${pagePath}`)
|
||||
)!;
|
||||
);
|
||||
const alternative = lookup.urls[0]!;
|
||||
|
||||
expect(
|
||||
getURLLookupPathname(lookup, {
|
||||
getURLLookupPathname(alternative, {
|
||||
basePath: '/preview/site_example/section/',
|
||||
pathname: homepagePath,
|
||||
})
|
||||
@@ -26,10 +32,12 @@ describe('getURLLookupPathname', () => {
|
||||
it.each(['revisions', 'changes'])(
|
||||
'preserves the custom homepage at a %s preview root',
|
||||
(kind) => {
|
||||
const lookup = getURLLookup(new URL(`${previewRoot}/~/${kind}/revision-id/`))!;
|
||||
const lookup = getURLLookupAlternatives(
|
||||
new URL(`${previewRoot}/~/${kind}/revision-id/`)
|
||||
);
|
||||
|
||||
expect(
|
||||
getURLLookupPathname(lookup, {
|
||||
getURLLookupPathname(lookup.urls[0]!, {
|
||||
basePath: '/preview/site_example/section/',
|
||||
pathname: homepagePath,
|
||||
})
|
||||
@@ -37,22 +45,27 @@ describe('getURLLookupPathname', () => {
|
||||
}
|
||||
);
|
||||
|
||||
it('uses the resolved pathname for a full URL lookup', () => {
|
||||
const lookup = getURLLookup(new URL(`https://docs.example.com/section/${pagePath}`))!;
|
||||
it.each(['', '/section/variant'])('resolves a published page below the %s root', (basePath) => {
|
||||
const rootURL = `https://docs.example.com${basePath}`;
|
||||
const lookup = getURLLookupAlternatives(new URL(`${rootURL}/${pagePath}`));
|
||||
const alternative = lookup.urls.find(({ url }) => url === new URL(rootURL).toString())!;
|
||||
|
||||
expect(alternative).toBeDefined();
|
||||
expect(
|
||||
getURLLookupPathname(lookup, {
|
||||
basePath: '/section/',
|
||||
pathname: `/${pagePath}`,
|
||||
getURLLookupPathname(alternative, {
|
||||
basePath: `${basePath}/`,
|
||||
pathname: homepagePath,
|
||||
})
|
||||
).toBe(`/${pagePath}/`);
|
||||
).toBe(`/${pagePath}`);
|
||||
});
|
||||
|
||||
it('preserves page resolution without a custom homepage', () => {
|
||||
const lookup = getURLLookup(new URL(`${previewRoot}/~/revisions/revision-id/${pagePath}`))!;
|
||||
const lookup = getURLLookupAlternatives(
|
||||
new URL(`${previewRoot}/~/revisions/revision-id/${pagePath}`)
|
||||
);
|
||||
|
||||
expect(
|
||||
getURLLookupPathname(lookup, {
|
||||
getURLLookupPathname(lookup.urls[0]!, {
|
||||
basePath: '/preview/site_example/section/',
|
||||
pathname: '/',
|
||||
})
|
||||
@@ -75,90 +88,617 @@ describe('getURLLookupPathname', () => {
|
||||
);
|
||||
|
||||
it('preserves a legitimate page prefix for a non-root lookup', () => {
|
||||
const lookup = getURLLookupAlternatives(
|
||||
new URL('https://docs.example.com/section/guide/page')
|
||||
);
|
||||
const alternative = lookup.urls.find(
|
||||
({ url }) => url === 'https://docs.example.com/section/guide'
|
||||
)!;
|
||||
|
||||
expect(
|
||||
getURLLookupPathname(
|
||||
{ url: 'https://docs.example.com/section/guide', extraPath: 'page' },
|
||||
{ basePath: '/section/', pathname: '/guide' }
|
||||
)
|
||||
getURLLookupPathname(alternative, {
|
||||
basePath: '/section/',
|
||||
pathname: '/guide',
|
||||
})
|
||||
).toBe('/guide/page');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getURLLookup', () => {
|
||||
it.each([
|
||||
'https://docs.mycompany.com/',
|
||||
'https://docs.mycompany.com/a/b/c/d/e',
|
||||
'https://test.gitbook.io/space',
|
||||
'https://test.gitbook.io/v/variant/space',
|
||||
'https://test.gitbook.io/sharelink/v/variant/space',
|
||||
'https://docs.mycompany.com/a/~/b/c/d',
|
||||
'https://docs.mycompany.com/section/variant/~gitbook/ogimage/pageId',
|
||||
'https://org.gitbook.io/~gitbook/icon',
|
||||
'https://docs.mycompany.com/llms-full.txt/100',
|
||||
'https://docs.mycompany.com/section/blog/rss.xml',
|
||||
'https://docs.mycompany.com/a/b/intro.md',
|
||||
'https://proxy.gitbook.site/sites/site_foo/hello/world',
|
||||
])('looks up the full URL %s', (url) => {
|
||||
expect(getURLLookup(new URL(url))).toEqual({ url, extraPath: '' });
|
||||
describe('getURLLookupAlternatives', () => {
|
||||
it('should return all URLs up to the root', () => {
|
||||
expect(getURLLookupAlternatives(new URL('https://docs.mycompany.com/a/b/c'))).toEqual({
|
||||
revision: undefined,
|
||||
changeRequest: undefined,
|
||||
basePath: undefined,
|
||||
urls: [
|
||||
{
|
||||
extraPath: 'a/b/c',
|
||||
url: 'https://docs.mycompany.com/',
|
||||
primary: false,
|
||||
},
|
||||
{
|
||||
extraPath: 'b/c',
|
||||
url: 'https://docs.mycompany.com/a',
|
||||
primary: false,
|
||||
},
|
||||
{
|
||||
extraPath: 'c',
|
||||
url: 'https://docs.mycompany.com/a/b',
|
||||
primary: false,
|
||||
},
|
||||
{
|
||||
extraPath: '',
|
||||
url: 'https://docs.mycompany.com/a/b/c',
|
||||
primary: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(getURLLookupAlternatives(new URL('https://docs.mycompany.com/a/b/c/d'))).toEqual({
|
||||
revision: undefined,
|
||||
changeRequest: undefined,
|
||||
basePath: undefined,
|
||||
urls: [
|
||||
{
|
||||
extraPath: 'a/b/c/d',
|
||||
url: 'https://docs.mycompany.com/',
|
||||
primary: false,
|
||||
},
|
||||
{
|
||||
extraPath: 'b/c/d',
|
||||
url: 'https://docs.mycompany.com/a',
|
||||
primary: false,
|
||||
},
|
||||
{
|
||||
extraPath: 'c/d',
|
||||
url: 'https://docs.mycompany.com/a/b',
|
||||
primary: false,
|
||||
},
|
||||
{
|
||||
extraPath: 'd',
|
||||
url: 'https://docs.mycompany.com/a/b/c',
|
||||
primary: false,
|
||||
},
|
||||
{
|
||||
extraPath: '',
|
||||
url: 'https://docs.mycompany.com/a/b/c/d',
|
||||
primary: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('looks up the normalized URL', () => {
|
||||
expect(getURLLookup(new URL('https://docs.mycompany.com//hello/'))).toEqual({
|
||||
url: 'https://docs.mycompany.com/hello',
|
||||
extraPath: '',
|
||||
it('should not match before the variant for a variant url', () => {
|
||||
expect(
|
||||
getURLLookupAlternatives(new URL('https://test.gitbook.io/v/variant/space'))
|
||||
).toEqual({
|
||||
revision: undefined,
|
||||
changeRequest: undefined,
|
||||
basePath: undefined,
|
||||
urls: [
|
||||
{
|
||||
url: 'https://test.gitbook.io/v/variant',
|
||||
extraPath: 'space',
|
||||
primary: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should not match before the variant for a variant in a share link', () => {
|
||||
expect(
|
||||
getURLLookupAlternatives(new URL('https://test.gitbook.io/sharelink/v/variant/space'))
|
||||
).toEqual({
|
||||
revision: undefined,
|
||||
changeRequest: undefined,
|
||||
basePath: undefined,
|
||||
urls: [
|
||||
{
|
||||
url: 'https://test.gitbook.io/sharelink/v/variant',
|
||||
extraPath: 'space',
|
||||
primary: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should not match before a revision in a variant', () => {
|
||||
expect(
|
||||
getURLLookupAlternatives(
|
||||
new URL('https://test.gitbook.io/v/variant/~/revisions/id/rest')
|
||||
)
|
||||
).toEqual({
|
||||
revision: 'id',
|
||||
changeRequest: undefined,
|
||||
basePath: '~/revisions/id',
|
||||
urls: [
|
||||
{
|
||||
url: 'https://test.gitbook.io/v/variant',
|
||||
extraPath: 'rest',
|
||||
primary: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should not match before a revision ID', () => {
|
||||
expect(getURLLookup(new URL('https://docs.mycompany.com/~/revisions/id/a/b/c'))).toEqual({
|
||||
url: 'https://docs.mycompany.com/',
|
||||
extraPath: 'a/b/c',
|
||||
basePath: '~/revisions/id',
|
||||
expect(
|
||||
getURLLookupAlternatives(new URL('https://docs.mycompany.com/~/revisions/id/a/b/c'))
|
||||
).toEqual({
|
||||
revision: 'id',
|
||||
changeRequest: undefined,
|
||||
basePath: '~/revisions/id',
|
||||
urls: [
|
||||
{
|
||||
extraPath: 'a/b/c',
|
||||
url: 'https://docs.mycompany.com/',
|
||||
primary: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should not match before a change request ID', () => {
|
||||
expect(getURLLookup(new URL('https://docs.mycompany.com/~/changes/id/hello'))).toEqual({
|
||||
url: 'https://docs.mycompany.com/',
|
||||
extraPath: 'hello',
|
||||
basePath: '~/changes/id',
|
||||
expect(
|
||||
getURLLookupAlternatives(new URL('https://docs.mycompany.com/~/changes/id/hello'))
|
||||
).toEqual({
|
||||
revision: undefined,
|
||||
changeRequest: 'id',
|
||||
basePath: '~/changes/id',
|
||||
urls: [
|
||||
{
|
||||
extraPath: 'hello',
|
||||
url: 'https://docs.mycompany.com/',
|
||||
primary: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should normalize duplicated slashes', () => {
|
||||
expect(getURLLookupAlternatives(new URL('https://docs.mycompany.com//hello'))).toEqual({
|
||||
revision: undefined,
|
||||
changeRequest: undefined,
|
||||
basePath: undefined,
|
||||
urls: [
|
||||
{
|
||||
extraPath: 'hello',
|
||||
url: 'https://docs.mycompany.com/',
|
||||
primary: false,
|
||||
},
|
||||
{
|
||||
extraPath: '',
|
||||
url: 'https://docs.mycompany.com/hello',
|
||||
primary: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should normalize trailing slash', () => {
|
||||
expect(getURLLookupAlternatives(new URL('https://docs.mycompany.com/hello/'))).toEqual({
|
||||
revision: undefined,
|
||||
changeRequest: undefined,
|
||||
basePath: undefined,
|
||||
urls: [
|
||||
{
|
||||
extraPath: 'hello',
|
||||
url: 'https://docs.mycompany.com/',
|
||||
primary: false,
|
||||
},
|
||||
{
|
||||
extraPath: '',
|
||||
url: 'https://docs.mycompany.com/hello',
|
||||
primary: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should match a variant in a share-link', () => {
|
||||
expect(
|
||||
getURLLookupAlternatives(new URL('https://test.gitbook.io/sharelink/v/variant/space'))
|
||||
).toEqual({
|
||||
revision: undefined,
|
||||
changeRequest: undefined,
|
||||
basePath: undefined,
|
||||
urls: [
|
||||
{
|
||||
url: 'https://test.gitbook.io/sharelink/v/variant',
|
||||
extraPath: 'space',
|
||||
primary: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should match a revision in a variant in a share-link', () => {
|
||||
expect(
|
||||
getURLLookup(
|
||||
getURLLookupAlternatives(
|
||||
new URL('https://test.gitbook.io/sharelink/v/variant/~/revisions/id/a/b/c')
|
||||
)
|
||||
).toEqual({
|
||||
url: 'https://test.gitbook.io/sharelink/v/variant',
|
||||
extraPath: 'a/b/c',
|
||||
basePath: '~/revisions/id',
|
||||
revision: 'id',
|
||||
changeRequest: undefined,
|
||||
basePath: '~/revisions/id',
|
||||
urls: [
|
||||
{
|
||||
url: 'https://test.gitbook.io/sharelink/v/variant',
|
||||
extraPath: 'a/b/c',
|
||||
primary: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should match a change request in a variant in a share-link', () => {
|
||||
expect(
|
||||
getURLLookupAlternatives(
|
||||
new URL('https://test.gitbook.io/sharelink/v/variant/~/changes/id/a/b/c')
|
||||
)
|
||||
).toEqual({
|
||||
revision: undefined,
|
||||
changeRequest: 'id',
|
||||
basePath: '~/changes/id',
|
||||
urls: [
|
||||
{
|
||||
url: 'https://test.gitbook.io/sharelink/v/variant',
|
||||
extraPath: 'a/b/c',
|
||||
primary: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should limit depth', () => {
|
||||
expect(getURLLookupAlternatives(new URL('https://docs.mycompany.com/a/b/c/d/e'))).toEqual({
|
||||
revision: undefined,
|
||||
changeRequest: undefined,
|
||||
basePath: undefined,
|
||||
urls: [
|
||||
{
|
||||
extraPath: 'a/b/c/d/e',
|
||||
url: 'https://docs.mycompany.com/',
|
||||
primary: false,
|
||||
},
|
||||
{
|
||||
extraPath: 'b/c/d/e',
|
||||
url: 'https://docs.mycompany.com/a',
|
||||
primary: false,
|
||||
},
|
||||
{
|
||||
extraPath: 'c/d/e',
|
||||
url: 'https://docs.mycompany.com/a/b',
|
||||
primary: false,
|
||||
},
|
||||
{
|
||||
extraPath: 'd/e',
|
||||
url: 'https://docs.mycompany.com/a/b/c',
|
||||
primary: false,
|
||||
},
|
||||
{
|
||||
extraPath: 'e',
|
||||
url: 'https://docs.mycompany.com/a/b/c/d',
|
||||
primary: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should ignore dummy ~', () => {
|
||||
expect(getURLLookupAlternatives(new URL('https://docs.mycompany.com/a/~/b/c/d'))).toEqual({
|
||||
revision: undefined,
|
||||
changeRequest: undefined,
|
||||
basePath: undefined,
|
||||
urls: [
|
||||
{
|
||||
extraPath: 'a/~/b/c/d',
|
||||
url: 'https://docs.mycompany.com/',
|
||||
primary: false,
|
||||
},
|
||||
{
|
||||
extraPath: '~/b/c/d',
|
||||
url: 'https://docs.mycompany.com/a',
|
||||
primary: false,
|
||||
},
|
||||
{
|
||||
extraPath: 'b/c/d',
|
||||
url: 'https://docs.mycompany.com/a/~',
|
||||
primary: false,
|
||||
},
|
||||
{
|
||||
extraPath: 'c/d',
|
||||
url: 'https://docs.mycompany.com/a/~/b',
|
||||
primary: false,
|
||||
},
|
||||
{
|
||||
extraPath: 'd',
|
||||
primary: true,
|
||||
url: 'https://docs.mycompany.com/a/~/b/c',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should match a root gitbook.io domain', () => {
|
||||
expect(getURLLookupAlternatives(new URL('https://test.gitbook.io/'))).toEqual({
|
||||
revision: undefined,
|
||||
changeRequest: undefined,
|
||||
basePath: undefined,
|
||||
urls: [
|
||||
{
|
||||
url: 'https://test.gitbook.io/',
|
||||
extraPath: '',
|
||||
primary: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip root gitbook.io domain if a path is present', () => {
|
||||
expect(getURLLookupAlternatives(new URL('https://test.gitbook.io/space'))).toEqual({
|
||||
revision: undefined,
|
||||
changeRequest: undefined,
|
||||
basePath: undefined,
|
||||
urls: [
|
||||
{
|
||||
url: 'https://test.gitbook.io/space',
|
||||
extraPath: '',
|
||||
primary: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should match a root custom domain', () => {
|
||||
expect(getURLLookupAlternatives(new URL('https://docs.mycompany.com/'))).toEqual({
|
||||
revision: undefined,
|
||||
changeRequest: undefined,
|
||||
basePath: undefined,
|
||||
urls: [
|
||||
{
|
||||
url: 'https://docs.mycompany.com/',
|
||||
extraPath: '',
|
||||
primary: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip proxy root URLs', () => {
|
||||
expect(
|
||||
getURLLookupAlternatives(
|
||||
new URL('https://proxy.gitbook.site/sites/site_foo/hello/world')
|
||||
)
|
||||
).toEqual({
|
||||
revision: undefined,
|
||||
changeRequest: undefined,
|
||||
basePath: undefined,
|
||||
urls: [
|
||||
{
|
||||
url: 'https://proxy.gitbook.site/sites/site_foo',
|
||||
extraPath: 'hello/world',
|
||||
primary: false,
|
||||
},
|
||||
{
|
||||
url: 'https://proxy.gitbook.site/sites/site_foo/hello',
|
||||
extraPath: 'world',
|
||||
primary: false,
|
||||
},
|
||||
{
|
||||
url: 'https://proxy.gitbook.site/sites/site_foo/hello/world',
|
||||
extraPath: '',
|
||||
primary: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should not match internal paths as part of the site URL', () => {
|
||||
expect(
|
||||
getURLLookupAlternatives(
|
||||
new URL('https://docs.mycompany.com/section/variant/~gitbook/ogimage/pageId')
|
||||
)
|
||||
).toEqual({
|
||||
revision: undefined,
|
||||
changeRequest: undefined,
|
||||
basePath: undefined,
|
||||
urls: [
|
||||
{
|
||||
url: 'https://docs.mycompany.com/',
|
||||
extraPath: 'section/variant/~gitbook/ogimage/pageId',
|
||||
primary: false,
|
||||
},
|
||||
{
|
||||
url: 'https://docs.mycompany.com/section',
|
||||
extraPath: 'variant/~gitbook/ogimage/pageId',
|
||||
primary: false,
|
||||
},
|
||||
{
|
||||
url: 'https://docs.mycompany.com/section/variant',
|
||||
extraPath: '~gitbook/ogimage/pageId',
|
||||
primary: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should match the root of a gitbook.io domain for an internal path', () => {
|
||||
expect(getURLLookupAlternatives(new URL('https://org.gitbook.io/~gitbook/icon'))).toEqual({
|
||||
revision: undefined,
|
||||
changeRequest: undefined,
|
||||
basePath: undefined,
|
||||
urls: [
|
||||
{
|
||||
url: 'https://org.gitbook.io/',
|
||||
extraPath: '~gitbook/icon',
|
||||
primary: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(
|
||||
getURLLookupAlternatives(new URL('https://org.gitbook.io/docs/~gitbook/search'))
|
||||
).toEqual({
|
||||
revision: undefined,
|
||||
changeRequest: undefined,
|
||||
basePath: undefined,
|
||||
urls: [
|
||||
{
|
||||
url: 'https://org.gitbook.io/docs',
|
||||
extraPath: '~gitbook/search',
|
||||
primary: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should not match site root files', () => {
|
||||
for (const file of ['llms.txt', 'llms-full.txt/100', 'robots.txt', 'sitemap.xml']) {
|
||||
expect(getURLLookupAlternatives(new URL(`https://docs.mycompany.com/${file}`))).toEqual(
|
||||
{
|
||||
revision: undefined,
|
||||
changeRequest: undefined,
|
||||
basePath: undefined,
|
||||
urls: [
|
||||
{
|
||||
url: 'https://docs.mycompany.com/',
|
||||
extraPath: file,
|
||||
primary: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should not match a RSS feed at the end of a page path', () => {
|
||||
expect(
|
||||
getURLLookupAlternatives(new URL('https://docs.mycompany.com/section/blog/rss.xml'))
|
||||
).toEqual({
|
||||
revision: undefined,
|
||||
changeRequest: undefined,
|
||||
basePath: undefined,
|
||||
urls: [
|
||||
{
|
||||
url: 'https://docs.mycompany.com/',
|
||||
extraPath: 'section/blog/rss.xml',
|
||||
primary: false,
|
||||
},
|
||||
{
|
||||
url: 'https://docs.mycompany.com/section',
|
||||
extraPath: 'blog/rss.xml',
|
||||
primary: false,
|
||||
},
|
||||
{
|
||||
url: 'https://docs.mycompany.com/section/blog',
|
||||
extraPath: 'rss.xml',
|
||||
primary: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should keep matching up to the variant for an internal path', () => {
|
||||
expect(
|
||||
getURLLookupAlternatives(
|
||||
new URL('https://test.gitbook.io/sharelink/v/variant/~gitbook/search')
|
||||
)
|
||||
).toEqual({
|
||||
revision: undefined,
|
||||
changeRequest: undefined,
|
||||
basePath: undefined,
|
||||
urls: [
|
||||
{
|
||||
url: 'https://test.gitbook.io/sharelink/v/variant',
|
||||
extraPath: '~gitbook/search',
|
||||
primary: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should keep the revision base path for an internal path', () => {
|
||||
expect(
|
||||
getURLLookup(
|
||||
getURLLookupAlternatives(
|
||||
new URL('https://docs.mycompany.com/~/revisions/id/a/~gitbook/ogimage/pageId')
|
||||
)
|
||||
).toEqual({
|
||||
url: 'https://docs.mycompany.com/',
|
||||
extraPath: 'a/~gitbook/ogimage/pageId',
|
||||
basePath: '~/revisions/id',
|
||||
revision: 'id',
|
||||
changeRequest: undefined,
|
||||
basePath: '~/revisions/id',
|
||||
urls: [
|
||||
{
|
||||
url: 'https://docs.mycompany.com/',
|
||||
extraPath: 'a/~gitbook/ogimage/pageId',
|
||||
primary: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
'https://proxy.gitbook.site/',
|
||||
'https://proxy.gitbook.site/sites',
|
||||
'https://proxy.gitbook.site/sites/~/changes/id/hello',
|
||||
])('should skip the proxy root URL %s', (url) => {
|
||||
expect(getURLLookup(new URL(url))).toBeNull();
|
||||
it('should only match internal paths at the end of the URL', () => {
|
||||
expect(
|
||||
getURLLookupAlternatives(new URL('https://docs.mycompany.com/a/llms.txt/b'))
|
||||
).toEqual({
|
||||
revision: undefined,
|
||||
changeRequest: undefined,
|
||||
basePath: undefined,
|
||||
urls: [
|
||||
{
|
||||
url: 'https://docs.mycompany.com/',
|
||||
extraPath: 'a/llms.txt/b',
|
||||
primary: false,
|
||||
},
|
||||
{
|
||||
url: 'https://docs.mycompany.com/a',
|
||||
extraPath: 'llms.txt/b',
|
||||
primary: false,
|
||||
},
|
||||
{
|
||||
url: 'https://docs.mycompany.com/a/llms.txt',
|
||||
extraPath: 'b',
|
||||
primary: false,
|
||||
},
|
||||
{
|
||||
url: 'https://docs.mycompany.com/a/llms.txt/b',
|
||||
extraPath: '',
|
||||
primary: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should not treat a markdown page path as internal', () => {
|
||||
expect(
|
||||
getURLLookupAlternatives(new URL('https://docs.mycompany.com/a/b/intro.md'))
|
||||
).toEqual({
|
||||
revision: undefined,
|
||||
changeRequest: undefined,
|
||||
basePath: undefined,
|
||||
urls: [
|
||||
{
|
||||
url: 'https://docs.mycompany.com/',
|
||||
extraPath: 'a/b/intro.md',
|
||||
primary: false,
|
||||
},
|
||||
{
|
||||
url: 'https://docs.mycompany.com/a',
|
||||
extraPath: 'b/intro.md',
|
||||
primary: false,
|
||||
},
|
||||
{
|
||||
url: 'https://docs.mycompany.com/a/b',
|
||||
extraPath: 'intro.md',
|
||||
primary: false,
|
||||
},
|
||||
{
|
||||
url: 'https://docs.mycompany.com/a/b/intro.md',
|
||||
extraPath: '',
|
||||
primary: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -297,3 +837,54 @@ describe('normalizeURL with encoded paths', () => {
|
||||
expect(result.searchParams.get('filter')).toBe(risonValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getURLLookupAlternatives with bypass', () => {
|
||||
it('only looks up the full URL', () => {
|
||||
expect(
|
||||
getURLLookupAlternatives(new URL('https://docs.mycompany.com/a/b/c'), { bypass: true })
|
||||
).toEqual({
|
||||
revision: undefined,
|
||||
changeRequest: undefined,
|
||||
basePath: undefined,
|
||||
urls: [{ url: 'https://docs.mycompany.com/a/b/c', extraPath: '', primary: true }],
|
||||
});
|
||||
});
|
||||
|
||||
it('only looks up the full URL for a variant', () => {
|
||||
expect(
|
||||
getURLLookupAlternatives(new URL('https://test.gitbook.io/v/variant/space'), {
|
||||
bypass: true,
|
||||
}).urls
|
||||
).toEqual([
|
||||
{ url: 'https://test.gitbook.io/v/variant/space', extraPath: '', primary: true },
|
||||
]);
|
||||
});
|
||||
|
||||
it.each(['revisions', 'changes'])('keeps the alternatives for %s', (kind) => {
|
||||
const url = new URL(`https://docs.mycompany.com/a/~/${kind}/id/page`);
|
||||
expect(getURLLookupAlternatives(url, { bypass: true })).toEqual(
|
||||
getURLLookupAlternatives(url, { bypass: false })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldBypassLookupAlternatives', () => {
|
||||
const bypassURLs = ['https://docs.mycompany.com/section', 'https://other.mycompany.com/'];
|
||||
|
||||
it.each([
|
||||
'https://docs.mycompany.com/section',
|
||||
'https://docs.mycompany.com/section/page',
|
||||
'https://other.mycompany.com/',
|
||||
'https://other.mycompany.com/page',
|
||||
])('matches %s', (url) => {
|
||||
expect(shouldBypassLookupAlternatives(normalizeURL(new URL(url)), bypassURLs)).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
'https://docs.mycompany.com/sectionpage',
|
||||
'https://docs.mycompany.com/',
|
||||
'https://unknown.mycompany.com/section',
|
||||
])('does not match %s', (url) => {
|
||||
expect(shouldBypassLookupAlternatives(normalizeURL(new URL(url)), bypassURLs)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { GITBOOK_DISABLE_LOOKUP_ALTERNATIVES } from '../env';
|
||||
import { joinPath, removeTrailingSlash } from '../paths';
|
||||
import { isProxyRootRequest } from '../proxy';
|
||||
import { DataFetcherError, getExposableError } from './errors';
|
||||
|
||||
/**
|
||||
* Paths served by our own routes rather than by site content. They can never be part of a
|
||||
* site URL, so a revision or change marker is never searched for inside them.
|
||||
* site URL, so we stop matching there instead of spending lookups on URLs that cannot resolve.
|
||||
* Kept in sync with the site route handlers and `encodePathInSiteContent` in the middleware.
|
||||
*/
|
||||
const INTERNAL_PATHS = [
|
||||
@@ -43,48 +44,156 @@ function getContentPathSegments(pathSegments: string[]): string[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* For a given GitBook URL, return the URL to resolve with the API.
|
||||
*
|
||||
* The full URL is resolved as-is, except for revisions and changes in the path
|
||||
* (`~/revisions/<id>/` or `~/changes/<id>/`): these are resolved up to the tilde,
|
||||
* and the rest of the path goes in the extraPath.
|
||||
*
|
||||
* Returns `null` for the root proxy URLs, which can never resolve to a site.
|
||||
* Site URL prefixes resolved with the full URL only, for sites where a shorter alternative
|
||||
* would resolve to the wrong content.
|
||||
*/
|
||||
export function getURLLookup(input: URL): {
|
||||
url: string;
|
||||
extraPath: string;
|
||||
basePath?: string;
|
||||
changeRequest?: string;
|
||||
revision?: string;
|
||||
} | null {
|
||||
const LOOKUP_ALTERNATIVES_BYPASS_URLS: string[] = ['https://proxy.gitbook.site/sites/site_p4Xo4'];
|
||||
|
||||
/**
|
||||
* Whether the lookup of this (normalized) URL should skip the shorter alternatives.
|
||||
*/
|
||||
export function shouldBypassLookupAlternatives(
|
||||
url: URL,
|
||||
bypassURLs: string[] = LOOKUP_ALTERNATIVES_BYPASS_URLS
|
||||
): boolean {
|
||||
if (GITBOOK_DISABLE_LOOKUP_ALTERNATIVES) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return bypassURLs.some((bypassURL) => {
|
||||
const prefix = normalizeURL(new URL(bypassURL));
|
||||
const prefixPath = removeTrailingSlash(prefix.pathname);
|
||||
return (
|
||||
prefix.origin === url.origin &&
|
||||
(removeTrailingSlash(url.pathname) === prefixPath ||
|
||||
url.pathname.startsWith(`${prefixPath}/`))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* For a given GitBook URL, return a list of alternative URLs that could be matched against to lookup the content.
|
||||
* The approach is optimized to aim at reusing cached lookup results as much as possible.
|
||||
*
|
||||
* The cases can be:
|
||||
*
|
||||
* Custom hostname
|
||||
* - Public content has a custom hostname: docs.company.com/<path>
|
||||
* - Public content has a custom hostname with a share-link: docs.company.com/<link>/<path>
|
||||
* - Public content has a custom hostname with a variant: docs.company.com/v/<variant>/<path>
|
||||
* - Public content has a custom hostname with a variant and a share-link: docs.company.com/<link>/v/<variant>/<path>
|
||||
* - Revisions in a custom hostname: docs.company.com/~/revisions/<id>/<path>
|
||||
* - Changes in a custom hostname: docs.company.com/~/changes/<id>/<path>
|
||||
*
|
||||
* Custom hostname in the organization or GitBook domain (company.gitbook.io)
|
||||
* - Public content has a custom hostname in the organization: docs.company.com/<space>/<path>
|
||||
* - Public content has a custom hostname in the organization with a share-link: docs.company.com/<space>/<link>/<path>
|
||||
* - Public content has a custom hostname in the organization with a variant: docs.company.com/<space>/v/<variant>/<path>
|
||||
* - Public content has a custom hostname in the organization with a variant and a share-link: docs.company.com/<space>/<link>/v/<variant>/<path>
|
||||
*/
|
||||
export function getURLLookupAlternatives(input: URL, options: { bypass?: boolean } = {}) {
|
||||
const url = normalizeURL(input);
|
||||
const bypass = options.bypass ?? shouldBypassLookupAlternatives(url);
|
||||
|
||||
let basePath: string | undefined = undefined;
|
||||
let changeRequest: string | undefined = undefined;
|
||||
let revision: string | undefined = undefined;
|
||||
const alternatives: { url: string; extraPath: string; primary: boolean }[] = [];
|
||||
|
||||
const pushAlternative = (adding: URL, extraPath: string) => {
|
||||
const existing = alternatives.find((alt) => alt.url === adding.toString());
|
||||
if (existing) {
|
||||
if (existing.extraPath !== extraPath) {
|
||||
throw new Error(
|
||||
`Invalid extraPath ${extraPath} for url ${adding.toString()}, already set to ${
|
||||
existing.extraPath
|
||||
}`
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// We don't want to push the url if it is the root proxy URL (either https://proxy.gitbook.site or https://proxy.gitbook.site/sites)
|
||||
if (isProxyRootRequest(adding)) {
|
||||
return;
|
||||
}
|
||||
|
||||
alternatives.push({
|
||||
url: adding.toString(),
|
||||
extraPath,
|
||||
primary: adding.toString() === url.toString(),
|
||||
});
|
||||
};
|
||||
|
||||
const pathSegments = url.pathname.slice(1).split('/');
|
||||
// A `~` in an internal path (like `~gitbook/...`) is not a revision or change marker.
|
||||
// Only the content part of the path can be matched against a site URL, the rest goes in the extraPath.
|
||||
const contentSegments = getContentPathSegments(pathSegments);
|
||||
const tildeIndex = contentSegments.indexOf('~');
|
||||
const kind = tildeIndex >= 0 ? contentSegments[tildeIndex + 1] : undefined;
|
||||
const vIndex = contentSegments.indexOf('v');
|
||||
|
||||
if (kind !== 'revisions' && kind !== 'changes') {
|
||||
return isProxyRootRequest(url) ? null : { url: url.toString(), extraPath: '' };
|
||||
// URL looks like a specific content url (with ~/revisions/ or ~/changes/ in the path)
|
||||
// We only start matching after the ~/revisions/ or ~/changes/ segment and we ignore everything before it
|
||||
if (
|
||||
tildeIndex >= 0 &&
|
||||
(contentSegments[tildeIndex + 1] === 'revisions' ||
|
||||
contentSegments[tildeIndex + 1] === 'changes')
|
||||
) {
|
||||
const revisionOrChangeIdIndex = tildeIndex + 2;
|
||||
|
||||
basePath = contentSegments.slice(tildeIndex, revisionOrChangeIdIndex + 1).join('/');
|
||||
if (contentSegments[tildeIndex + 1] === 'revisions') {
|
||||
revision = contentSegments[revisionOrChangeIdIndex];
|
||||
} else {
|
||||
changeRequest = contentSegments[revisionOrChangeIdIndex];
|
||||
}
|
||||
|
||||
// Match up to the tilde
|
||||
const contentURL = new URL(url);
|
||||
contentURL.pathname = contentSegments.slice(0, tildeIndex).join('/');
|
||||
pushAlternative(contentURL, pathSegments.slice(revisionOrChangeIdIndex + 1).join('/'));
|
||||
}
|
||||
|
||||
const idIndex = tildeIndex + 2;
|
||||
const id = contentSegments[idIndex];
|
||||
const contentURL = new URL(url);
|
||||
contentURL.pathname = contentSegments.slice(0, tildeIndex).join('/');
|
||||
|
||||
if (isProxyRootRequest(contentURL)) {
|
||||
return null;
|
||||
// Revisions and changes above still need their alternatives to extract the base path.
|
||||
else if (bypass) {
|
||||
pushAlternative(url, '');
|
||||
}
|
||||
|
||||
return {
|
||||
url: contentURL.toString(),
|
||||
extraPath: pathSegments.slice(idIndex + 1).join('/'),
|
||||
basePath: contentSegments.slice(tildeIndex, idIndex + 1).join('/'),
|
||||
...(kind === 'revisions' ? { revision: id } : { changeRequest: id }),
|
||||
};
|
||||
// URL looks like a collection url (with /v/ in the path)
|
||||
// We only start matching after the /v/ segment and we ignore everything before it
|
||||
// to avoid potentially matching as a page not found under the default space in the collection
|
||||
else if (vIndex >= 0 && contentSegments.length >= vIndex + 2) {
|
||||
const collectionURL = new URL(url);
|
||||
collectionURL.pathname = contentSegments.slice(0, vIndex + 2).join('/');
|
||||
|
||||
pushAlternative(collectionURL, pathSegments.slice(vIndex + 2).join('/'));
|
||||
} else {
|
||||
// Match only with the host, if it can be a custom hostname
|
||||
// It should cover most cases of custom domains, and with caching, it should be fast.
|
||||
if (!url.hostname.includes('.gitbook.io') || contentSegments.length === 0) {
|
||||
const noPathURL = new URL(url);
|
||||
noPathURL.pathname = '/';
|
||||
|
||||
pushAlternative(noPathURL, url.pathname.slice(1));
|
||||
}
|
||||
|
||||
// Otherwise match with the first four segments of the path
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
if (contentSegments.length >= i) {
|
||||
const shortURL = new URL(url);
|
||||
shortURL.pathname = contentSegments.slice(0, i).join('/');
|
||||
|
||||
pushAlternative(shortURL, pathSegments.slice(i).join('/'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mark the longuest entry to lookup as primary
|
||||
alternatives.sort((a, b) => b.extraPath.length - a.extraPath.length);
|
||||
if (alternatives.length > 0) {
|
||||
alternatives[alternatives.length - 1]!.primary = true;
|
||||
}
|
||||
|
||||
return { urls: alternatives, basePath, changeRequest, revision };
|
||||
}
|
||||
|
||||
/** Combine a resolved lookup with the remaining requested page path. */
|
||||
|
||||
+6
@@ -93,6 +93,12 @@ export const GITBOOK_DISABLE_TRACKING = Boolean(
|
||||
*/
|
||||
export const GITBOOK_DISABLE_INSIGHTS = process.env.GITBOOK_DISABLE_INSIGHTS === 'true';
|
||||
|
||||
/**
|
||||
* Whether to resolve site content with the full URL only, skipping the shorter lookup alternatives.
|
||||
*/
|
||||
export const GITBOOK_DISABLE_LOOKUP_ALTERNATIVES =
|
||||
process.env.GITBOOK_DISABLE_LOOKUP_ALTERNATIVES === 'true';
|
||||
|
||||
/**
|
||||
* Hostname serving the integrations.
|
||||
*/
|
||||
|
||||
@@ -334,18 +334,6 @@ describe('buildSiteMcpServerCard', () => {
|
||||
expect(search).not.toHaveProperty('inputSchema');
|
||||
});
|
||||
|
||||
it('gives example page URLs under the path of a subpath site', () => {
|
||||
const context = makeContext({ host: 'docs.example.com', siteBasePath: '/product/' });
|
||||
const tools = createSiteMcpTools(context, { request });
|
||||
const getPage = tools.find((tool) => tool.name === 'getPage');
|
||||
const pageUrl = tools.find((tool) => tool.name === 'sendFeedback')?.inputSchema.pageUrl;
|
||||
const example = 'https://docs.example.com/product/getting-started';
|
||||
|
||||
expect(getPage?.description).toContain(`(e.g. ${example})`);
|
||||
expect(pageUrl?.description).toContain(`(e.g. ${example})`);
|
||||
expect(pageUrl?.parse('getting-started')).toBe(example);
|
||||
});
|
||||
|
||||
it('omits askQuestion when the site has no AI mode', () => {
|
||||
const withAI = buildCard({ aiMode: CustomizationAIMode.Assistant });
|
||||
const withoutAI = buildCard({ aiMode: CustomizationAIMode.None });
|
||||
|
||||
@@ -51,7 +51,6 @@ export function createSiteMcpTools(
|
||||
const { request } = options;
|
||||
const { dataFetcher, linker, site } = context;
|
||||
const siteUrl = context.siteSpace.urls.published;
|
||||
const examplePageURL = siteUrl ? joinPathWithBaseURL(siteUrl, 'getting-started') : undefined;
|
||||
|
||||
const tools: SiteMcpTool[] = [
|
||||
defineTool({
|
||||
@@ -147,7 +146,7 @@ export function createSiteMcpTools(
|
||||
|
||||
defineTool({
|
||||
name: 'getPage',
|
||||
description: `Fetch the full markdown content of a specific documentation page from ${site.title}. Use this when you have a page URL and want to read its content. Accepts full URLs (e.g. ${examplePageURL}). Since \`searchDocumentation\` returns partial content, use \`getPage\` to retrieve the complete page when you need more details. The content includes links you can follow to navigate to related pages.`,
|
||||
description: `Fetch the full markdown content of a specific documentation page from ${site.title}. Use this when you have a page URL and want to read its content. Accepts full URLs (e.g. ${siteUrl}/getting-started). Since \`searchDocumentation\` returns partial content, use \`getPage\` to retrieve the complete page when you need more details. The content includes links you can follow to navigate to related pages.`,
|
||||
inputSchema: {
|
||||
url: z
|
||||
.string()
|
||||
@@ -161,7 +160,7 @@ export function createSiteMcpTools(
|
||||
}
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `"${value}" is not a valid URL. Expected a full URL like ${examplePageURL}`,
|
||||
message: `"${value}" is not a valid URL. Expected a full URL like ${siteUrl}/getting-started`,
|
||||
});
|
||||
return z.NEVER;
|
||||
}),
|
||||
@@ -357,7 +356,7 @@ export function createSiteMcpTools(
|
||||
if (!url) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `"${value}" is not a valid URL on this site. Expected a full URL like ${examplePageURL}`,
|
||||
message: `"${value}" is not a valid URL on this site. Expected a full URL like ${siteUrl}/getting-started`,
|
||||
});
|
||||
return z.NEVER;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
getVisitorUnsignedClaims,
|
||||
isRevalidationRequest,
|
||||
normalizeVisitorURL,
|
||||
parseHostnameList,
|
||||
shouldSendVisitorCountry,
|
||||
} from './visitors';
|
||||
|
||||
describe('getVisitorAuthToken', () => {
|
||||
@@ -660,3 +662,32 @@ describe('getVisitorCountry', () => {
|
||||
).toBe('FR');
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldSendVisitorCountry', () => {
|
||||
it.each(['dev', 'preview', 'staging'])('should be enabled on the %p stage', (stage) => {
|
||||
expect(shouldSendVisitorCountry('docs.example.com', stage)).toBe(true);
|
||||
});
|
||||
|
||||
it.each(['production', undefined])(
|
||||
'should be disabled for other hostnames on the %p stage',
|
||||
(stage) => {
|
||||
expect(shouldSendVisitorCountry('docs.example.com', stage)).toBe(false);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('parseHostnameList', () => {
|
||||
it.each([undefined, '', ' , '])('should return an empty set for %p', (value) => {
|
||||
expect(parseHostnameList(value).size).toBe(0);
|
||||
});
|
||||
|
||||
it('should parse a single entry', () => {
|
||||
expect(parseHostnameList('docs.foo.com')).toEqual(new Set(['docs.foo.com']));
|
||||
});
|
||||
|
||||
it('should trim, lowercase and drop empty entries', () => {
|
||||
expect(parseHostnameList(' Docs.Foo.com , ,bar.io')).toEqual(
|
||||
new Set(['docs.foo.com', 'bar.io'])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -106,6 +106,33 @@ export function getVisitorType(request: {
|
||||
return detection.detected && detection.method !== 'heuristic' ? 'agent' : 'human';
|
||||
}
|
||||
|
||||
// Production hostnames for which the visitor country is sent while the feature is rolled out.
|
||||
// Set at build time from a GitHub Actions variable to keep customer hostnames out of the code.
|
||||
const VISITOR_COUNTRY_HOSTNAMES = parseHostnameList(process.env.GITBOOK_VISITOR_COUNTRY_HOSTNAMES);
|
||||
const VISITOR_COUNTRY_STAGES = new Set(['dev', 'preview', 'staging']);
|
||||
|
||||
/**
|
||||
* Parse a comma-separated list of hostnames.
|
||||
*/
|
||||
export function parseHostnameList(value: string | undefined): Set<string> {
|
||||
return new Set(
|
||||
(value ?? '')
|
||||
.split(',')
|
||||
.map((hostname) => hostname.trim().toLowerCase())
|
||||
.filter(Boolean)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the visitor country should be sent when resolving the site URL.
|
||||
*/
|
||||
export function shouldSendVisitorCountry(hostname: string, stage: string | undefined): boolean {
|
||||
return (
|
||||
(!!stage && VISITOR_COUNTRY_STAGES.has(stage)) ||
|
||||
VISITOR_COUNTRY_HOSTNAMES.has(hostname.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ISO 3166-1 alpha-2 country code of the visitor from the geolocation headers,
|
||||
* or from `x-gitbook-country` when the request comes from the revalidation worker.
|
||||
|
||||
@@ -35,7 +35,12 @@ import {
|
||||
normalizeRequestURL,
|
||||
throwIfDataError,
|
||||
} from '@/lib/data';
|
||||
import { GITBOOK_DISABLE_INSIGHTS, isGitBookAssetsHostURL, isGitBookHostURL } from '@/lib/env';
|
||||
import {
|
||||
GITBOOK_DISABLE_INSIGHTS,
|
||||
GITBOOK_STAGE,
|
||||
isGitBookAssetsHostURL,
|
||||
isGitBookHostURL,
|
||||
} from '@/lib/env';
|
||||
import { getImageResizingContextId } from '@/lib/images';
|
||||
import { isAITrainingOrIndexingRequest } from '@/lib/indexing-crawlers';
|
||||
import { MCP_SERVER_CARD_PATH, MCP_SERVER_CARD_WELL_KNOWN_PATH } from '@/lib/mcp/paths';
|
||||
@@ -62,6 +67,7 @@ import {
|
||||
isRevalidationRequest,
|
||||
normalizeVisitorURL,
|
||||
serveVisitorClaimsDataRequest,
|
||||
shouldSendVisitorCountry,
|
||||
} from '@/lib/visitors';
|
||||
import { waitUntil } from '@/lib/waitUntil';
|
||||
import { serveResizedImage } from '@/routes/image';
|
||||
@@ -221,7 +227,9 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
|
||||
//
|
||||
request.headers.delete('x-gitbook-disable-tracking');
|
||||
|
||||
const visitorCountry = getVisitorCountry(request);
|
||||
const visitorCountry = shouldSendVisitorCountry(siteRequestURL.hostname, GITBOOK_STAGE)
|
||||
? getVisitorCountry(request)
|
||||
: undefined;
|
||||
|
||||
const withAPIToken = async (apiToken: string | null) => {
|
||||
const siteURLData = await throwIfDataError(
|
||||
@@ -571,7 +579,7 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
|
||||
|
||||
// noindex search/assistant deep links, kept crawlable so Google sees the directive.
|
||||
if (rewrittenURL.searchParams.has('ask') || rewrittenURL.searchParams.has('q')) {
|
||||
response.headers.set('x-robots-tag', 'none');
|
||||
response.headers.set('x-robots-tag', 'noindex');
|
||||
}
|
||||
|
||||
// Allow cross-origin requests from the same parent domain as the site.
|
||||
|
||||
Reference in New Issue
Block a user