mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-10-27 15:37:05 +00:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 11ef360467 | |||
| 7d5a6d2255 | |||
| 30e9e18ab4 | |||
| a1da4f023d | |||
| 666e8426e1 | |||
| df2fa42bda | |||
| b3a7ad6d2c | |||
| 2932077bf9 | |||
| 957afd967f | |||
| 4c9a9d0416 | |||
| 40df91a363 | |||
| af66ff71ed | |||
| 6f8ecd6111 | |||
| fa3eb07617 | |||
| dc4268db64 | |||
| a0c06a72cc | |||
| 19c20c0be3 | |||
| fae34e56ac | |||
| fcae8b0bf3 | |||
| a47fb6c2bd | |||
| d86eb47bf7 | |||
| ba0094a862 | |||
| b259009c9c | |||
| d88dd5ed8a | |||
| 57bb146075 | |||
| 4b67fe5317 |
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"gitbook": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix nested a tag causing hydration error
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"gitbook": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix href being empty in TOC
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"gitbook": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix navigation between sections/variants when previewing a site in v2
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'@gitbook/react-openapi': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Add authorization header for OAuth2
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"gitbook-v2": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
add a force-revalidate api route to force bust the cache in case of errors
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"gitbook-v2": patch
|
||||||
|
"gitbook": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix invalid HTML on an Announcement banner without a CTA.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"gitbook-v2": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix ISR on preview env
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'@gitbook/react-openapi': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Indent JSON python code sample
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"gitbook": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Make TOC height dynamic based on visible header and footer elements
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"gitbook": patch
|
||||||
|
"gitbook-v2": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
cache fonts and static image used in OGImage in memory
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'@gitbook/react-openapi': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Handle nested deprecated properties in generateSchemaExample
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"gitbook-v2": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Add `urlObject.hash` to `linker.toLinkForContent` to pass through URL fragment identifiers, used in search
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'@gitbook/react-openapi': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Deduplicate path parameters from OpenAPI spec
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"gitbook-v2": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
remove trailing slash from linker
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
name: Gradual Deploy to Cloudflare
|
||||||
|
description: Use gradual deployment to deploy to Cloudflare. This action will upload the middleware and server versions to Cloudflare and kept them bound together
|
||||||
|
inputs:
|
||||||
|
apiToken:
|
||||||
|
description: 'Cloudflare API token'
|
||||||
|
required: true
|
||||||
|
accountId:
|
||||||
|
description: 'Cloudflare account ID'
|
||||||
|
required: true
|
||||||
|
environment:
|
||||||
|
description: 'Cloudflare environment to deploy to (staging, production, preview)'
|
||||||
|
required: true
|
||||||
|
middlewareVersionId:
|
||||||
|
description: 'Middleware version ID to deploy'
|
||||||
|
required: true
|
||||||
|
serverVersionId:
|
||||||
|
description: 'Server version ID to deploy'
|
||||||
|
required: true
|
||||||
|
outputs:
|
||||||
|
deployment-url:
|
||||||
|
description: "Deployment URL"
|
||||||
|
value: ${{ steps.deploy_middleware.outputs.deployment-url }}
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- id: wrangler_status
|
||||||
|
name: Check wrangler deployment status
|
||||||
|
uses: cloudflare/wrangler-action@v3.14.0
|
||||||
|
with:
|
||||||
|
apiToken: ${{ inputs.apiToken }}
|
||||||
|
accountId: ${{ inputs.accountId }}
|
||||||
|
workingDirectory: ./
|
||||||
|
wranglerVersion: '4.10.0'
|
||||||
|
environment: ${{ inputs.environment }}
|
||||||
|
command: deployments status --config ./packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc
|
||||||
|
|
||||||
|
# This step is used to get the version ID that is currently deployed to Cloudflare.
|
||||||
|
- id: extract_current_version
|
||||||
|
name: Extract current version
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
version_id=$(echo "${{ steps.wrangler_status.outputs.command-output }}" | grep -A 3 "(100%)" | grep -oP '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')
|
||||||
|
echo "version_id=$version_id" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- id: deploy_server
|
||||||
|
name: Deploy server to Cloudflare at 0%
|
||||||
|
uses: cloudflare/wrangler-action@v3.14.0
|
||||||
|
with:
|
||||||
|
apiToken: ${{ inputs.apiToken }}
|
||||||
|
accountId: ${{ inputs.accountId }}
|
||||||
|
workingDirectory: ./
|
||||||
|
wranglerVersion: '4.10.0'
|
||||||
|
environment: ${{ inputs.environment }}
|
||||||
|
command: versions deploy ${{ steps.extract_current_version.outputs.version_id }}@100% ${{ inputs.serverVersionId }}@0% -y --config ./packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc
|
||||||
|
|
||||||
|
# Since we use version overrides headers, we can directly deploy the middleware to 100%.
|
||||||
|
- id: deploy_middleware
|
||||||
|
name: Deploy middleware to Cloudflare at 100%
|
||||||
|
uses: cloudflare/wrangler-action@v3.14.0
|
||||||
|
with:
|
||||||
|
apiToken: ${{ inputs.apiToken }}
|
||||||
|
accountId: ${{ inputs.accountId }}
|
||||||
|
workingDirectory: ./
|
||||||
|
wranglerVersion: '4.10.0'
|
||||||
|
environment: ${{ inputs.environment }}
|
||||||
|
command: versions deploy ${{ inputs.middlewareVersionId }}@100% -y --config ./packages/gitbook-v2/openNext/customWorkers/middlewareWrangler.jsonc
|
||||||
|
|
||||||
|
- name: Deploy server to Cloudflare at 100%
|
||||||
|
uses: cloudflare/wrangler-action@v3.14.0
|
||||||
|
with:
|
||||||
|
apiToken: ${{ inputs.apiToken }}
|
||||||
|
accountId: ${{ inputs.accountId }}
|
||||||
|
workingDirectory: ./
|
||||||
|
wranglerVersion: '4.10.0'
|
||||||
|
environment: ${{ inputs.environment }}
|
||||||
|
command: versions deploy ${{ inputs.serverVersionId }}@100% -y --config ./packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc
|
||||||
|
|
||||||
|
- name: Outputs
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
DEPLOYMENT_URL: ${{ steps.deploy_middleware.outputs.deployment-url }}
|
||||||
|
run: |
|
||||||
|
echo "URL: ${{ steps.deploy_middleware.outputs.deployment-url }}"
|
||||||
@@ -28,7 +28,7 @@ inputs:
|
|||||||
outputs:
|
outputs:
|
||||||
deployment-url:
|
deployment-url:
|
||||||
description: "Deployment URL"
|
description: "Deployment URL"
|
||||||
value: ${{ steps.deploy.outputs.deployment-url }}
|
value: ${{ steps.upload_middleware.outputs.deployment-url }}
|
||||||
runs:
|
runs:
|
||||||
using: 'composite'
|
using: 'composite'
|
||||||
steps:
|
steps:
|
||||||
@@ -63,8 +63,8 @@ runs:
|
|||||||
env:
|
env:
|
||||||
GITBOOK_RUNTIME: cloudflare
|
GITBOOK_RUNTIME: cloudflare
|
||||||
shell: bash
|
shell: bash
|
||||||
- id: deploy
|
|
||||||
name: Deploy to Cloudflare
|
- name: Upload the DO worker
|
||||||
uses: cloudflare/wrangler-action@v3.14.0
|
uses: cloudflare/wrangler-action@v3.14.0
|
||||||
with:
|
with:
|
||||||
apiToken: ${{ inputs.apiToken }}
|
apiToken: ${{ inputs.apiToken }}
|
||||||
@@ -72,10 +72,67 @@ runs:
|
|||||||
workingDirectory: ./
|
workingDirectory: ./
|
||||||
wranglerVersion: '4.10.0'
|
wranglerVersion: '4.10.0'
|
||||||
environment: ${{ inputs.environment }}
|
environment: ${{ inputs.environment }}
|
||||||
command: ${{ inputs.deploy == 'true' && 'deploy' || format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook-v2/wrangler.jsonc
|
command: deploy --config ./packages/gitbook-v2/openNext/customWorkers/doWrangler.jsonc
|
||||||
|
|
||||||
|
- id: upload_server
|
||||||
|
name: Upload server to Cloudflare
|
||||||
|
uses: cloudflare/wrangler-action@v3.14.0
|
||||||
|
with:
|
||||||
|
apiToken: ${{ inputs.apiToken }}
|
||||||
|
accountId: ${{ inputs.accountId }}
|
||||||
|
workingDirectory: ./
|
||||||
|
wranglerVersion: '4.10.0'
|
||||||
|
environment: ${{ inputs.environment }}
|
||||||
|
command: ${{ format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc
|
||||||
|
|
||||||
|
- name: Extract server version worker ID
|
||||||
|
shell: bash
|
||||||
|
id: extract_server_version_id
|
||||||
|
run: |
|
||||||
|
version_id=$(echo '${{ steps.upload_server.outputs.command-output }}' | grep "Worker Version ID" | awk '{print $4}')
|
||||||
|
echo "version_id=$version_id" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Run updateWrangler scripts
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
bun run ./packages/gitbook-v2/openNext/customWorkers/script/updateWrangler.ts ${{ steps.extract_server_version_id.outputs.version_id }}
|
||||||
|
|
||||||
|
- id: upload_middleware
|
||||||
|
name: Upload middleware to Cloudflare
|
||||||
|
uses: cloudflare/wrangler-action@v3.14.0
|
||||||
|
with:
|
||||||
|
apiToken: ${{ inputs.apiToken }}
|
||||||
|
accountId: ${{ inputs.accountId }}
|
||||||
|
workingDirectory: ./
|
||||||
|
wranglerVersion: '4.10.0'
|
||||||
|
environment: ${{ inputs.environment }}
|
||||||
|
command: ${{ format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook-v2/openNext/customWorkers/middlewareWrangler.jsonc
|
||||||
|
|
||||||
|
- name: Extract middleware version worker ID
|
||||||
|
shell: bash
|
||||||
|
id: extract_middleware_version_id
|
||||||
|
run: |
|
||||||
|
version_id=$(echo '${{ steps.upload_middleware.outputs.command-output }}' | grep "Worker Version ID" | awk '{print $4}')
|
||||||
|
echo "version_id=$version_id" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Deploy server and middleware to Cloudflare
|
||||||
|
if: ${{ inputs.deploy == 'true' }}
|
||||||
|
uses: ./.github/actions/gradual-deploy-cloudflare
|
||||||
|
with:
|
||||||
|
apiToken: ${{ inputs.apiToken }}
|
||||||
|
accountId: ${{ inputs.accountId }}
|
||||||
|
opServiceAccount: ${{ inputs.opServiceAccount }}
|
||||||
|
opItem: ${{ inputs.opItem }}
|
||||||
|
environment: ${{ inputs.environment }}
|
||||||
|
serverVersionId: ${{ steps.extract_server_version_id.outputs.version_id }}
|
||||||
|
middlewareVersionId: ${{ steps.extract_middleware_version_id.outputs.version_id }}
|
||||||
|
deploy: ${{ inputs.deploy }}
|
||||||
|
|
||||||
|
|
||||||
- name: Outputs
|
- name: Outputs
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
DEPLOYMENT_URL: ${{ steps.deploy.outputs.deployment-url }}
|
DEPLOYMENT_URL: ${{ steps.upload_middleware.outputs.deployment-url }}
|
||||||
run: |
|
run: |
|
||||||
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
|
echo "URL: ${{ steps.upload_middleware.outputs.deployment-url }}"
|
||||||
|
echo "Output server: ${{ steps.upload_server.outputs.command-output }}"
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
},
|
},
|
||||||
"packages/colors": {
|
"packages/colors": {
|
||||||
"name": "@gitbook/colors",
|
"name": "@gitbook/colors",
|
||||||
"version": "0.3.2",
|
"version": "0.3.3",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.5.3",
|
"typescript": "^5.5.3",
|
||||||
},
|
},
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
},
|
},
|
||||||
"packages/gitbook": {
|
"packages/gitbook": {
|
||||||
"name": "gitbook",
|
"name": "gitbook",
|
||||||
"version": "0.11.1",
|
"version": "0.12.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@gitbook/api": "^0.115.0",
|
"@gitbook/api": "^0.115.0",
|
||||||
"@gitbook/cache-do": "workspace:*",
|
"@gitbook/cache-do": "workspace:*",
|
||||||
@@ -141,11 +141,11 @@
|
|||||||
},
|
},
|
||||||
"packages/gitbook-v2": {
|
"packages/gitbook-v2": {
|
||||||
"name": "gitbook-v2",
|
"name": "gitbook-v2",
|
||||||
"version": "0.2.5",
|
"version": "0.3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@gitbook/api": "^0.115.0",
|
"@gitbook/api": "^0.115.0",
|
||||||
"@gitbook/cache-tags": "workspace:*",
|
"@gitbook/cache-tags": "workspace:*",
|
||||||
"@opennextjs/cloudflare": "1.0.3",
|
"@opennextjs/cloudflare": "1.0.4",
|
||||||
"@sindresorhus/fnv1a": "^3.1.0",
|
"@sindresorhus/fnv1a": "^3.1.0",
|
||||||
"assert-never": "^1.2.1",
|
"assert-never": "^1.2.1",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
@@ -185,7 +185,7 @@
|
|||||||
},
|
},
|
||||||
"packages/openapi-parser": {
|
"packages/openapi-parser": {
|
||||||
"name": "@gitbook/openapi-parser",
|
"name": "@gitbook/openapi-parser",
|
||||||
"version": "2.1.3",
|
"version": "2.1.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@scalar/openapi-parser": "^0.10.10",
|
"@scalar/openapi-parser": "^0.10.10",
|
||||||
"@scalar/openapi-types": "^0.1.9",
|
"@scalar/openapi-types": "^0.1.9",
|
||||||
@@ -232,7 +232,7 @@
|
|||||||
},
|
},
|
||||||
"packages/react-openapi": {
|
"packages/react-openapi": {
|
||||||
"name": "@gitbook/react-openapi",
|
"name": "@gitbook/react-openapi",
|
||||||
"version": "1.2.1",
|
"version": "1.3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@gitbook/openapi-parser": "workspace:*",
|
"@gitbook/openapi-parser": "workspace:*",
|
||||||
"@scalar/api-client-react": "^1.2.19",
|
"@scalar/api-client-react": "^1.2.19",
|
||||||
@@ -791,9 +791,9 @@
|
|||||||
|
|
||||||
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
||||||
|
|
||||||
"@opennextjs/aws": ["@opennextjs/aws@3.6.1", "", { "dependencies": { "@ast-grep/napi": "^0.35.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "esbuild": "0.19.2", "express": "5.0.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.0.0", "yaml": "^2.7.0" }, "bin": { "open-next": "dist/index.js" } }, "sha512-RYU9K58vEUPXqc3pZO6kr9vBy1MmJZFQZLe0oXBskC005oGju/m4e3DCCP4eZ/Q/HdYQXCoqNXgSGi8VCAYgew=="],
|
"@opennextjs/aws": ["@opennextjs/aws@3.6.2", "", { "dependencies": { "@ast-grep/napi": "^0.35.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "esbuild": "0.25.4", "express": "5.0.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.0.0", "yaml": "^2.7.0" }, "bin": { "open-next": "dist/index.js" } }, "sha512-26/3GSoj7mKN7XpQFikYM2Lrwal5jlMc4fJO//QAdd5bCSnBaWBAkzr7+VvXAFVIC6eBeDLdtlWiQuUQVEAPZQ=="],
|
||||||
|
|
||||||
"@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.0.3", "", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "^3.6.1", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6" }, "peerDependencies": { "wrangler": "^4.14.0" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-+SNhz2JmKkMlWzndTSYZeq0eseAPfEXUUYJA7H9MkgFe36i1no0OdiLuwu9e4uR3yuOzaNWB+skCYHsD6Im9nA=="],
|
"@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.0.4", "", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "^3.6.2", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6" }, "peerDependencies": { "wrangler": "^4.14.0" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-DVudGpOSJ91ruPiBJ0kuFmPhEQbXIXLTbvUjAx1OlbwFskG2gvdNIAmF3ZXV6z1VGDO7Q/u2W2ybMZLf7avlrA=="],
|
||||||
|
|
||||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||||
|
|
||||||
@@ -3649,7 +3649,7 @@
|
|||||||
|
|
||||||
"@node-minify/core/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
|
"@node-minify/core/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
|
||||||
|
|
||||||
"@opennextjs/aws/esbuild": ["esbuild@0.19.2", "", { "optionalDependencies": { "@esbuild/android-arm": "0.19.2", "@esbuild/android-arm64": "0.19.2", "@esbuild/android-x64": "0.19.2", "@esbuild/darwin-arm64": "0.19.2", "@esbuild/darwin-x64": "0.19.2", "@esbuild/freebsd-arm64": "0.19.2", "@esbuild/freebsd-x64": "0.19.2", "@esbuild/linux-arm": "0.19.2", "@esbuild/linux-arm64": "0.19.2", "@esbuild/linux-ia32": "0.19.2", "@esbuild/linux-loong64": "0.19.2", "@esbuild/linux-mips64el": "0.19.2", "@esbuild/linux-ppc64": "0.19.2", "@esbuild/linux-riscv64": "0.19.2", "@esbuild/linux-s390x": "0.19.2", "@esbuild/linux-x64": "0.19.2", "@esbuild/netbsd-x64": "0.19.2", "@esbuild/openbsd-x64": "0.19.2", "@esbuild/sunos-x64": "0.19.2", "@esbuild/win32-arm64": "0.19.2", "@esbuild/win32-ia32": "0.19.2", "@esbuild/win32-x64": "0.19.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-G6hPax8UbFakEj3hWO0Vs52LQ8k3lnBhxZWomUJDxfz3rZTLqF5k/FCzuNdLx2RbpBiQQF9H9onlDDH1lZsnjg=="],
|
"@opennextjs/aws/esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="],
|
||||||
|
|
||||||
"@radix-ui/react-collection/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="],
|
"@radix-ui/react-collection/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="],
|
||||||
|
|
||||||
@@ -4721,49 +4721,55 @@
|
|||||||
|
|
||||||
"@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=="],
|
"@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=="],
|
||||||
|
|
||||||
"@opennextjs/aws/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.19.2", "", { "os": "android", "cpu": "arm" }, "sha512-tM8yLeYVe7pRyAu9VMi/Q7aunpLwD139EY1S99xbQkT4/q2qa6eA4ige/WJQYdJ8GBL1K33pPFhPfPdJ/WzT8Q=="],
|
"@opennextjs/aws/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="],
|
||||||
|
|
||||||
"@opennextjs/aws/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.19.2", "", { "os": "android", "cpu": "arm64" }, "sha512-lsB65vAbe90I/Qe10OjkmrdxSX4UJDjosDgb8sZUKcg3oefEuW2OT2Vozz8ef7wrJbMcmhvCC+hciF8jY/uAkw=="],
|
"@opennextjs/aws/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.4", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="],
|
||||||
|
|
||||||
"@opennextjs/aws/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.19.2", "", { "os": "android", "cpu": "x64" }, "sha512-qK/TpmHt2M/Hg82WXHRc/W/2SGo/l1thtDHZWqFq7oi24AjZ4O/CpPSu6ZuYKFkEgmZlFoa7CooAyYmuvnaG8w=="],
|
"@opennextjs/aws/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.4", "", { "os": "android", "cpu": "arm64" }, "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A=="],
|
||||||
|
|
||||||
"@opennextjs/aws/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.19.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Ora8JokrvrzEPEpZO18ZYXkH4asCdc1DLdcVy8TGf5eWtPO1Ie4WroEJzwI52ZGtpODy3+m0a2yEX9l+KUn0tA=="],
|
"@opennextjs/aws/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.4", "", { "os": "android", "cpu": "x64" }, "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ=="],
|
||||||
|
|
||||||
"@opennextjs/aws/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.19.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-tP+B5UuIbbFMj2hQaUr6EALlHOIOmlLM2FK7jeFBobPy2ERdohI4Ka6ZFjZ1ZYsrHE/hZimGuU90jusRE0pwDw=="],
|
"@opennextjs/aws/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g=="],
|
||||||
|
|
||||||
"@opennextjs/aws/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.19.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-YbPY2kc0acfzL1VPVK6EnAlig4f+l8xmq36OZkU0jzBVHcOTyQDhnKQaLzZudNJQyymd9OqQezeaBgkTGdTGeQ=="],
|
"@opennextjs/aws/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A=="],
|
||||||
|
|
||||||
"@opennextjs/aws/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.19.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-nSO5uZT2clM6hosjWHAsS15hLrwCvIWx+b2e3lZ3MwbYSaXwvfO528OF+dLjas1g3bZonciivI8qKR/Hm7IWGw=="],
|
"@opennextjs/aws/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ=="],
|
||||||
|
|
||||||
"@opennextjs/aws/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.19.2", "", { "os": "linux", "cpu": "arm" }, "sha512-Odalh8hICg7SOD7XCj0YLpYCEc+6mkoq63UnExDCiRA2wXEmGlK5JVrW50vZR9Qz4qkvqnHcpH+OFEggO3PgTg=="],
|
"@opennextjs/aws/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ=="],
|
||||||
|
|
||||||
"@opennextjs/aws/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.19.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-ig2P7GeG//zWlU0AggA3pV1h5gdix0MA3wgB+NsnBXViwiGgY77fuN9Wr5uoCrs2YzaYfogXgsWZbm+HGr09xg=="],
|
"@opennextjs/aws/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.4", "", { "os": "linux", "cpu": "arm" }, "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ=="],
|
||||||
|
|
||||||
"@opennextjs/aws/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.19.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-mLfp0ziRPOLSTek0Gd9T5B8AtzKAkoZE70fneiiyPlSnUKKI4lp+mGEnQXcQEHLJAcIYDPSyBvsUbKUG2ri/XQ=="],
|
"@opennextjs/aws/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ=="],
|
||||||
|
|
||||||
"@opennextjs/aws/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.19.2", "", { "os": "linux", "cpu": "none" }, "sha512-hn28+JNDTxxCpnYjdDYVMNTR3SKavyLlCHHkufHV91fkewpIyQchS1d8wSbmXhs1fiYDpNww8KTFlJ1dHsxeSw=="],
|
"@opennextjs/aws/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.4", "", { "os": "linux", "cpu": "ia32" }, "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ=="],
|
||||||
|
|
||||||
"@opennextjs/aws/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.19.2", "", { "os": "linux", "cpu": "none" }, "sha512-KbXaC0Sejt7vD2fEgPoIKb6nxkfYW9OmFUK9XQE4//PvGIxNIfPk1NmlHmMg6f25x57rpmEFrn1OotASYIAaTg=="],
|
"@opennextjs/aws/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA=="],
|
||||||
|
|
||||||
"@opennextjs/aws/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.19.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-dJ0kE8KTqbiHtA3Fc/zn7lCd7pqVr4JcT0JqOnbj4LLzYnp+7h8Qi4yjfq42ZlHfhOCM42rBh0EwHYLL6LEzcw=="],
|
"@opennextjs/aws/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg=="],
|
||||||
|
|
||||||
"@opennextjs/aws/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.19.2", "", { "os": "linux", "cpu": "none" }, "sha512-7Z/jKNFufZ/bbu4INqqCN6DDlrmOTmdw6D0gH+6Y7auok2r02Ur661qPuXidPOJ+FSgbEeQnnAGgsVynfLuOEw=="],
|
"@opennextjs/aws/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag=="],
|
||||||
|
|
||||||
"@opennextjs/aws/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.19.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-U+RinR6aXXABFCcAY4gSlv4CL1oOVvSSCdseQmGO66H+XyuQGZIUdhG56SZaDJQcLmrSfRmx5XZOWyCJPRqS7g=="],
|
"@opennextjs/aws/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA=="],
|
||||||
|
|
||||||
"@opennextjs/aws/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.19.2", "", { "os": "linux", "cpu": "x64" }, "sha512-oxzHTEv6VPm3XXNaHPyUTTte+3wGv7qVQtqaZCrgstI16gCuhNOtBXLEBkBREP57YTd68P0VgDgG73jSD8bwXQ=="],
|
"@opennextjs/aws/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g=="],
|
||||||
|
|
||||||
"@opennextjs/aws/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.19.2", "", { "os": "none", "cpu": "x64" }, "sha512-WNa5zZk1XpTTwMDompZmvQLHszDDDN7lYjEHCUmAGB83Bgs20EMs7ICD+oKeT6xt4phV4NDdSi/8OfjPbSbZfQ=="],
|
"@opennextjs/aws/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.4", "", { "os": "linux", "cpu": "x64" }, "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA=="],
|
||||||
|
|
||||||
"@opennextjs/aws/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.19.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-S6kI1aT3S++Dedb7vxIuUOb3oAxqxk2Rh5rOXOTYnzN8JzW1VzBd+IqPiSpgitu45042SYD3HCoEyhLKQcDFDw=="],
|
"@opennextjs/aws/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.4", "", { "os": "none", "cpu": "arm64" }, "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ=="],
|
||||||
|
|
||||||
"@opennextjs/aws/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.19.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-VXSSMsmb+Z8LbsQGcBMiM+fYObDNRm8p7tkUDMPG/g4fhFX5DEFmjxIEa3N8Zr96SjsJ1woAhF0DUnS3MF3ARw=="],
|
"@opennextjs/aws/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.4", "", { "os": "none", "cpu": "x64" }, "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw=="],
|
||||||
|
|
||||||
"@opennextjs/aws/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.19.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-5NayUlSAyb5PQYFAU9x3bHdsqB88RC3aM9lKDAz4X1mo/EchMIT1Q+pSeBXNgkfNmRecLXA0O8xP+x8V+g/LKg=="],
|
"@opennextjs/aws/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A=="],
|
||||||
|
|
||||||
"@opennextjs/aws/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.19.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-47gL/ek1v36iN0wL9L4Q2MFdujR0poLZMJwhO2/N3gA89jgHp4MR8DKCmwYtGNksbfJb9JoTtbkoe6sDhg2QTA=="],
|
"@opennextjs/aws/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw=="],
|
||||||
|
|
||||||
"@opennextjs/aws/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.19.2", "", { "os": "win32", "cpu": "x64" }, "sha512-tcuhV7ncXBqbt/Ybf0IyrMcwVOAPDckMK9rXNHtF17UTK18OKLpg08glminN06pt2WCoALhXdLfSPbVvK/6fxw=="],
|
"@opennextjs/aws/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.4", "", { "os": "sunos", "cpu": "x64" }, "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q=="],
|
||||||
|
|
||||||
|
"@opennextjs/aws/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ=="],
|
||||||
|
|
||||||
|
"@opennextjs/aws/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg=="],
|
||||||
|
|
||||||
|
"@opennextjs/aws/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.4", "", { "os": "win32", "cpu": "x64" }, "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ=="],
|
||||||
|
|
||||||
"@radix-ui/react-dismissable-layer/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="],
|
"@radix-ui/react-dismissable-layer/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="],
|
||||||
|
|
||||||
|
|||||||
Vendored
+1
@@ -1,5 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
|
/// <reference types="next/navigation-types/compat/navigation" />
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
@@ -1,27 +1,29 @@
|
|||||||
import { defineCloudflareConfig } from '@opennextjs/cloudflare';
|
import type { OpenNextConfig } from '@opennextjs/cloudflare';
|
||||||
import doQueue from '@opennextjs/cloudflare/overrides/queue/do-queue';
|
|
||||||
import doShardedTagCache from '@opennextjs/cloudflare/overrides/tag-cache/do-sharded-tag-cache';
|
|
||||||
import {
|
|
||||||
softTagFilter,
|
|
||||||
withFilter,
|
|
||||||
} from '@opennextjs/cloudflare/overrides/tag-cache/tag-cache-filter';
|
|
||||||
|
|
||||||
export default defineCloudflareConfig({
|
export default {
|
||||||
incrementalCache: () => import('./openNext/incrementalCache').then((m) => m.default),
|
default: {
|
||||||
tagCache: withFilter({
|
override: {
|
||||||
tagCache: doShardedTagCache({
|
wrapper: 'cloudflare-node',
|
||||||
baseShardSize: 12,
|
converter: 'edge',
|
||||||
regionalCache: true,
|
proxyExternalRequest: 'fetch',
|
||||||
shardReplication: {
|
queue: () => import('./openNext/queue/server').then((m) => m.default),
|
||||||
numberOfSoftReplicas: 2,
|
incrementalCache: () => import('./openNext/incrementalCache').then((m) => m.default),
|
||||||
numberOfHardReplicas: 1,
|
tagCache: () => import('./openNext/tagCache/middleware').then((m) => m.default),
|
||||||
},
|
},
|
||||||
}),
|
},
|
||||||
// We don't use `revalidatePath`, so we filter out soft tags
|
middleware: {
|
||||||
filterFn: softTagFilter,
|
external: true,
|
||||||
}),
|
override: {
|
||||||
queue: doQueue,
|
wrapper: 'cloudflare-edge',
|
||||||
|
converter: 'edge',
|
||||||
// Performance improvements as we don't use PPR
|
proxyExternalRequest: 'fetch',
|
||||||
enableCacheInterception: true,
|
queue: () => import('./openNext/queue/middleware').then((m) => m.default),
|
||||||
});
|
incrementalCache: () => import('./openNext/incrementalCache').then((m) => m.default),
|
||||||
|
tagCache: () => import('./openNext/tagCache/middleware').then((m) => m.default),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dangerous: {
|
||||||
|
enableCacheInterception: true,
|
||||||
|
},
|
||||||
|
edgeExternals: ['node:crypto'],
|
||||||
|
} satisfies OpenNextConfig;
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { runWithCloudflareRequestContext } from '../../.open-next/cloudflare/init.js';
|
||||||
|
|
||||||
|
import { DurableObject } from 'cloudflare:workers';
|
||||||
|
|
||||||
|
// Only needed to run locally, in prod we'll use the one from do.js
|
||||||
|
export class R2WriteBuffer extends DurableObject {
|
||||||
|
writePromise;
|
||||||
|
|
||||||
|
async write(cacheKey, value) {
|
||||||
|
// We are already writing to this key
|
||||||
|
if (this.writePromise) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.writePromise = this.env.NEXT_INC_CACHE_R2_BUCKET.put(cacheKey, value);
|
||||||
|
this.ctx.waitUntil(
|
||||||
|
this.writePromise.finally(() => {
|
||||||
|
this.writePromise = undefined;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async fetch(request, env, ctx) {
|
||||||
|
return runWithCloudflareRequestContext(request, env, ctx, async () => {
|
||||||
|
// We can't move the handler import to the top level, otherwise the runtime will not be properly initialized
|
||||||
|
const { handler } = await import(
|
||||||
|
'../../.open-next/server-functions/default/handler.mjs'
|
||||||
|
);
|
||||||
|
|
||||||
|
// - `Request`s are handled by the Next server
|
||||||
|
return handler(request, env, ctx);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
{
|
||||||
|
"main": "default.js",
|
||||||
|
"name": "gitbook-open-v2-server",
|
||||||
|
"compatibility_date": "2025-04-14",
|
||||||
|
"compatibility_flags": [
|
||||||
|
"nodejs_compat",
|
||||||
|
"allow_importable_env",
|
||||||
|
"global_fetch_strictly_public"
|
||||||
|
],
|
||||||
|
"observability": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"vars": {
|
||||||
|
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true"
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"dev": {
|
||||||
|
"vars": {
|
||||||
|
"STAGE": "dev"
|
||||||
|
},
|
||||||
|
"r2_buckets": [
|
||||||
|
{
|
||||||
|
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||||
|
"bucket_name": "gitbook-open-v2-cache-preview"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"binding": "WORKER_SELF_REFERENCE",
|
||||||
|
"service": "gitbook-open-v2-server-dev"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"durable_objects": {
|
||||||
|
"bindings": [
|
||||||
|
{
|
||||||
|
"name": "WRITE_BUFFER",
|
||||||
|
"class_name": "R2WriteBuffer"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"migrations": [
|
||||||
|
{
|
||||||
|
"tag": "v1",
|
||||||
|
"new_sqlite_classes": ["R2WriteBuffer"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"preview": {
|
||||||
|
"vars": {
|
||||||
|
"STAGE": "preview",
|
||||||
|
// Just as a test for the preview environment to check that everything works
|
||||||
|
"NEXT_PRIVATE_DEBUG_CACHE": "true"
|
||||||
|
},
|
||||||
|
"r2_buckets": [
|
||||||
|
{
|
||||||
|
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||||
|
"bucket_name": "gitbook-open-v2-cache-preview"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"binding": "WORKER_SELF_REFERENCE",
|
||||||
|
"service": "gitbook-open-v2-server-preview"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"durable_objects": {
|
||||||
|
"bindings": [
|
||||||
|
{
|
||||||
|
"name": "WRITE_BUFFER",
|
||||||
|
"class_name": "R2WriteBuffer",
|
||||||
|
"script_name": "gitbook-open-v2-do-preview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "NEXT_TAG_CACHE_DO_SHARDED",
|
||||||
|
"class_name": "DOShardedTagCache",
|
||||||
|
"script_name": "gitbook-open-v2-do-preview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "NEXT_CACHE_DO_QUEUE",
|
||||||
|
"class_name": "DOQueueHandler",
|
||||||
|
"script_name": "gitbook-open-v2-do-preview"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"staging": {
|
||||||
|
"r2_buckets": [
|
||||||
|
{
|
||||||
|
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||||
|
"bucket_name": "gitbook-open-v2-cache-staging"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"binding": "WORKER_SELF_REFERENCE",
|
||||||
|
"service": "gitbook-open-v2-server-staging"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"durable_objects": {
|
||||||
|
"bindings": [
|
||||||
|
{
|
||||||
|
"name": "WRITE_BUFFER",
|
||||||
|
"class_name": "R2WriteBuffer",
|
||||||
|
"script_name": "gitbook-open-v2-do-staging"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "NEXT_TAG_CACHE_DO_SHARDED",
|
||||||
|
"class_name": "DOShardedTagCache",
|
||||||
|
"script_name": "gitbook-open-v2-do-staging"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "NEXT_CACHE_DO_QUEUE",
|
||||||
|
"class_name": "DOQueueHandler",
|
||||||
|
"script_name": "gitbook-open-v2-do-staging"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tail_consumers": [
|
||||||
|
{
|
||||||
|
"service": "gitbook-x-staging-tail"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"production": {
|
||||||
|
"r2_buckets": [
|
||||||
|
{
|
||||||
|
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||||
|
"bucket_name": "gitbook-open-v2-cache-production"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"binding": "WORKER_SELF_REFERENCE",
|
||||||
|
"service": "gitbook-open-v2-server-production"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"durable_objects": {
|
||||||
|
"bindings": [
|
||||||
|
{
|
||||||
|
"name": "WRITE_BUFFER",
|
||||||
|
"class_name": "R2WriteBuffer",
|
||||||
|
"script_name": "gitbook-open-v2-do-production"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "NEXT_TAG_CACHE_DO_SHARDED",
|
||||||
|
"class_name": "DOShardedTagCache",
|
||||||
|
"script_name": "gitbook-open-v2-do-production"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "NEXT_CACHE_DO_QUEUE",
|
||||||
|
"class_name": "DOQueueHandler",
|
||||||
|
"script_name": "gitbook-open-v2-do-production"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tail_consumers": [
|
||||||
|
{
|
||||||
|
"service": "gitbook-x-prod-tail"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
// This worker only purposes it to host the different DO that we will need in the other workers.
|
||||||
|
import { DurableObject } from 'cloudflare:workers';
|
||||||
|
|
||||||
|
// `use cache` could cause multiple writes to the same key to happen concurrently, there is a limit of 1 write per key/second
|
||||||
|
// so we need to buffer writes to the R2 bucket to avoid hitting this limit.
|
||||||
|
export class R2WriteBuffer extends DurableObject {
|
||||||
|
writePromise;
|
||||||
|
|
||||||
|
async write(cacheKey, value) {
|
||||||
|
// We are already writing to this key
|
||||||
|
if (this.writePromise) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.writePromise = this.env.NEXT_INC_CACHE_R2_BUCKET.put(cacheKey, value);
|
||||||
|
this.ctx.waitUntil(
|
||||||
|
this.writePromise.finally(() => {
|
||||||
|
this.writePromise = undefined;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { DOQueueHandler } from '../../.open-next/.build/durable-objects/queue.js';
|
||||||
|
|
||||||
|
export { DOShardedTagCache } from '../../.open-next/.build/durable-objects/sharded-tag-cache.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async fetch() {
|
||||||
|
// This worker does not handle any requests, it only provides Durable Objects
|
||||||
|
return new Response('This worker is not meant to handle requests directly', {
|
||||||
|
status: 400,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/plain',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
{
|
||||||
|
"main": "do.js",
|
||||||
|
"name": "gitbook-open-v2-do",
|
||||||
|
"compatibility_date": "2025-04-14",
|
||||||
|
"compatibility_flags": [
|
||||||
|
"nodejs_compat",
|
||||||
|
"allow_importable_env",
|
||||||
|
"global_fetch_strictly_public"
|
||||||
|
],
|
||||||
|
"observability": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"preview": {
|
||||||
|
"vars": {
|
||||||
|
"STAGE": "preview",
|
||||||
|
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true"
|
||||||
|
},
|
||||||
|
"r2_buckets": [
|
||||||
|
{
|
||||||
|
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||||
|
"bucket_name": "gitbook-open-v2-cache-preview"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"durable_objects": {
|
||||||
|
"bindings": [
|
||||||
|
{
|
||||||
|
"name": "NEXT_CACHE_DO_QUEUE",
|
||||||
|
"class_name": "DOQueueHandler"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "NEXT_TAG_CACHE_DO_SHARDED",
|
||||||
|
"class_name": "DOShardedTagCache"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "WRITE_BUFFER",
|
||||||
|
"class_name": "R2WriteBuffer"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"migrations": [
|
||||||
|
{
|
||||||
|
"tag": "v1",
|
||||||
|
"new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache", "R2WriteBuffer"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"staging": {
|
||||||
|
"vars": {
|
||||||
|
"STAGE": "staging",
|
||||||
|
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true"
|
||||||
|
},
|
||||||
|
"r2_buckets": [
|
||||||
|
{
|
||||||
|
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||||
|
"bucket_name": "gitbook-open-v2-cache-staging"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tail_consumers": [
|
||||||
|
{
|
||||||
|
"service": "gitbook-x-staging-tail"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"durable_objects": {
|
||||||
|
"bindings": [
|
||||||
|
{
|
||||||
|
"name": "NEXT_CACHE_DO_QUEUE",
|
||||||
|
"class_name": "DOQueueHandler"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "NEXT_TAG_CACHE_DO_SHARDED",
|
||||||
|
"class_name": "DOShardedTagCache"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "WRITE_BUFFER",
|
||||||
|
"class_name": "R2WriteBuffer"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"migrations": [
|
||||||
|
{
|
||||||
|
"tag": "v1",
|
||||||
|
"new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache", "R2WriteBuffer"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"production": {
|
||||||
|
"vars": {
|
||||||
|
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true",
|
||||||
|
"STAGE": "production"
|
||||||
|
},
|
||||||
|
"r2_buckets": [
|
||||||
|
{
|
||||||
|
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||||
|
"bucket_name": "gitbook-open-v2-cache-production"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tail_consumers": [
|
||||||
|
{
|
||||||
|
"service": "gitbook-x-prod-tail"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"durable_objects": {
|
||||||
|
"bindings": [
|
||||||
|
{
|
||||||
|
"name": "NEXT_CACHE_DO_QUEUE",
|
||||||
|
"class_name": "DOQueueHandler"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "NEXT_TAG_CACHE_DO_SHARDED",
|
||||||
|
"class_name": "DOShardedTagCache"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "WRITE_BUFFER",
|
||||||
|
"class_name": "R2WriteBuffer"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"migrations": [
|
||||||
|
{
|
||||||
|
"tag": "v1",
|
||||||
|
"new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache", "R2WriteBuffer"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { WorkerEntrypoint } from 'cloudflare:workers';
|
||||||
|
import { runWithCloudflareRequestContext } from '../../.open-next/cloudflare/init.js';
|
||||||
|
|
||||||
|
import { handler as middlewareHandler } from '../../.open-next/middleware/handler.mjs';
|
||||||
|
|
||||||
|
export { DOQueueHandler } from '../../.open-next/.build/durable-objects/queue.js';
|
||||||
|
|
||||||
|
export { DOShardedTagCache } from '../../.open-next/.build/durable-objects/sharded-tag-cache.js';
|
||||||
|
|
||||||
|
export default class extends WorkerEntrypoint {
|
||||||
|
async fetch(request) {
|
||||||
|
return runWithCloudflareRequestContext(request, this.env, this.ctx, async () => {
|
||||||
|
// - `Request`s are handled by the Next server
|
||||||
|
const reqOrResp = await middlewareHandler(request, this.env, this.ctx);
|
||||||
|
if (reqOrResp instanceof Response) {
|
||||||
|
return reqOrResp;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.env.STAGE !== 'preview') {
|
||||||
|
// https://developers.cloudflare.com/workers/configuration/versions-and-deployments/gradual-deployments/#version-affinity
|
||||||
|
reqOrResp.headers.set(
|
||||||
|
'Cloudflare-Workers-Version-Overrides',
|
||||||
|
`gitbook-open-v2-${this.env.STAGE}="${this.env.WORKER_VERSION_ID}"`
|
||||||
|
);
|
||||||
|
return this.env.DEFAULT_WORKER?.fetch(reqOrResp, {
|
||||||
|
cf: {
|
||||||
|
cacheEverything: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// If we are in preview mode, we need to send the request to the preview URL
|
||||||
|
const modifiedUrl = new URL(reqOrResp.url);
|
||||||
|
modifiedUrl.hostname = this.env.PREVIEW_HOSTNAME;
|
||||||
|
const nextRequest = new Request(modifiedUrl, reqOrResp);
|
||||||
|
return fetch(nextRequest, {
|
||||||
|
cf: {
|
||||||
|
cacheEverything: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
{
|
||||||
|
"main": "middleware.js",
|
||||||
|
"name": "gitbook-open-v2",
|
||||||
|
"compatibility_date": "2025-04-14",
|
||||||
|
"compatibility_flags": [
|
||||||
|
"nodejs_compat",
|
||||||
|
"allow_importable_env",
|
||||||
|
"global_fetch_strictly_public"
|
||||||
|
],
|
||||||
|
"assets": {
|
||||||
|
"directory": "../../.open-next/assets",
|
||||||
|
"binding": "ASSETS"
|
||||||
|
},
|
||||||
|
"observability": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"vars": {
|
||||||
|
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true"
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"dev": {
|
||||||
|
"vars": {
|
||||||
|
"STAGE": "dev",
|
||||||
|
"NEXT_PRIVATE_DEBUG_CACHE": "true"
|
||||||
|
},
|
||||||
|
"r2_buckets": [
|
||||||
|
{
|
||||||
|
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||||
|
"bucket_name": "gitbook-open-v2-cache-preview"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"binding": "WORKER_SELF_REFERENCE",
|
||||||
|
"service": "gitbook-open-v2-dev"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"binding": "DEFAULT_WORKER",
|
||||||
|
"service": "gitbook-open-v2-server-dev"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"preview": {
|
||||||
|
"vars": {
|
||||||
|
"STAGE": "preview",
|
||||||
|
"PREVIEW_HOSTNAME": "TO_REPLACE",
|
||||||
|
"WORKER_VERSION_ID": "TO_REPLACE"
|
||||||
|
},
|
||||||
|
"r2_buckets": [
|
||||||
|
{
|
||||||
|
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||||
|
"bucket_name": "gitbook-open-v2-cache-preview"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"binding": "WORKER_SELF_REFERENCE",
|
||||||
|
"service": "gitbook-open-v2-preview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"binding": "DEFAULT_WORKER",
|
||||||
|
"service": "gitbook-open-v2-server-preview"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"durable_objects": {
|
||||||
|
"bindings": [
|
||||||
|
{
|
||||||
|
"name": "WRITE_BUFFER",
|
||||||
|
"class_name": "R2WriteBuffer",
|
||||||
|
"script_name": "gitbook-open-v2-do-preview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "NEXT_TAG_CACHE_DO_SHARDED",
|
||||||
|
"class_name": "DOShardedTagCache",
|
||||||
|
"script_name": "gitbook-open-v2-do-preview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "NEXT_CACHE_DO_QUEUE",
|
||||||
|
"class_name": "DOQueueHandler",
|
||||||
|
"script_name": "gitbook-open-v2-do-preview"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"staging": {
|
||||||
|
"vars": {
|
||||||
|
"STAGE": "staging",
|
||||||
|
"WORKER_VERSION_ID": "TO_REPLACE"
|
||||||
|
},
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"pattern": "open-2c.gitbook-staging.com/*",
|
||||||
|
"zone_name": "gitbook-staging.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "static-2c.gitbook-staging.com/*",
|
||||||
|
"zone_name": "gitbook-staging.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"r2_buckets": [
|
||||||
|
{
|
||||||
|
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||||
|
"bucket_name": "gitbook-open-v2-cache-staging"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"binding": "WORKER_SELF_REFERENCE",
|
||||||
|
"service": "gitbook-open-v2-staging"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"binding": "DEFAULT_WORKER",
|
||||||
|
"service": "gitbook-open-v2-server-staging"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tail_consumers": [
|
||||||
|
{
|
||||||
|
"service": "gitbook-x-staging-tail"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"durable_objects": {
|
||||||
|
"bindings": [
|
||||||
|
{
|
||||||
|
"name": "WRITE_BUFFER",
|
||||||
|
"class_name": "R2WriteBuffer",
|
||||||
|
"script_name": "gitbook-open-v2-do-staging"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "NEXT_TAG_CACHE_DO_SHARDED",
|
||||||
|
"class_name": "DOShardedTagCache",
|
||||||
|
"script_name": "gitbook-open-v2-do-staging"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "NEXT_CACHE_DO_QUEUE",
|
||||||
|
"class_name": "DOQueueHandler",
|
||||||
|
"script_name": "gitbook-open-v2-do-staging"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"migrations": [
|
||||||
|
{
|
||||||
|
"tag": "v1",
|
||||||
|
"new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"production": {
|
||||||
|
"vars": {
|
||||||
|
// This is a bit misleading, but it means that we can have 500 concurrent revalidations
|
||||||
|
// This means that we'll have up to 100 durable objects instance running at the same time
|
||||||
|
"MAX_REVALIDATE_CONCURRENCY": "100",
|
||||||
|
// Temporary variable to find the issue once deployed
|
||||||
|
// TODO: remove this once the issue is fixed
|
||||||
|
"DEBUG_CLOUDFLARE": "true",
|
||||||
|
"WORKER_VERSION_ID": "TO_REPLACE",
|
||||||
|
"STAGE": "production"
|
||||||
|
},
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"pattern": "open-2c.gitbook.com/*",
|
||||||
|
"zone_name": "gitbook.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "static-2c.gitbook.com/*",
|
||||||
|
"zone_name": "gitbook.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"r2_buckets": [
|
||||||
|
{
|
||||||
|
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||||
|
"bucket_name": "gitbook-open-v2-cache-production"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"binding": "WORKER_SELF_REFERENCE",
|
||||||
|
"service": "gitbook-open-v2-production"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"binding": "DEFAULT_WORKER",
|
||||||
|
"service": "gitbook-open-v2-server-production"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tail_consumers": [
|
||||||
|
{
|
||||||
|
"service": "gitbook-x-prod-tail"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"durable_objects": {
|
||||||
|
"bindings": [
|
||||||
|
{
|
||||||
|
"name": "WRITE_BUFFER",
|
||||||
|
"class_name": "R2WriteBuffer",
|
||||||
|
"script_name": "gitbook-open-v2-do-production"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "NEXT_TAG_CACHE_DO_SHARDED",
|
||||||
|
"class_name": "DOShardedTagCache",
|
||||||
|
"script_name": "gitbook-open-v2-do-production"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "NEXT_CACHE_DO_QUEUE",
|
||||||
|
"class_name": "DOQueueHandler",
|
||||||
|
"script_name": "gitbook-open-v2-do-production"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"migrations": [
|
||||||
|
{
|
||||||
|
"tag": "v1",
|
||||||
|
"new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// In this script, we use the args from the cli to update the PREVIEW_URL vars in the wrangler config file for the middleware
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
const wranglerConfigPath = path.join(__dirname, '../middlewareWrangler.jsonc');
|
||||||
|
|
||||||
|
const file = fs.readFileSync(wranglerConfigPath, 'utf-8');
|
||||||
|
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
// The versionId is in the format xxx-xxx-xxx-xxx, we need the first part to reconstruct the preview URL
|
||||||
|
const versionId = args[0];
|
||||||
|
|
||||||
|
// The preview URL is in the format https://<versionId>-gitbook-open-v2-server-preview.gitbook.workers.dev
|
||||||
|
const previewHostname = `${versionId.split('-')[0]}-gitbook-open-v2-server-preview.gitbook.workers.dev`;
|
||||||
|
|
||||||
|
let updatedFile = file.replace(
|
||||||
|
/"PREVIEW_HOSTNAME": "TO_REPLACE"/,
|
||||||
|
`"PREVIEW_HOSTNAME": "${previewHostname}"`
|
||||||
|
);
|
||||||
|
|
||||||
|
updatedFile = updatedFile.replaceAll(
|
||||||
|
/"WORKER_VERSION_ID": "TO_REPLACE"/g,
|
||||||
|
`"WORKER_VERSION_ID": "${versionId}"`
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.writeFileSync(wranglerConfigPath, updatedFile);
|
||||||
@@ -9,6 +9,8 @@ import type {
|
|||||||
} from '@opennextjs/aws/types/overrides.js';
|
} from '@opennextjs/aws/types/overrides.js';
|
||||||
import { getCloudflareContext } from '@opennextjs/cloudflare';
|
import { getCloudflareContext } from '@opennextjs/cloudflare';
|
||||||
|
|
||||||
|
import type { DurableObjectNamespace, Rpc } from '@cloudflare/workers-types';
|
||||||
|
|
||||||
export const BINDING_NAME = 'NEXT_INC_CACHE_R2_BUCKET';
|
export const BINDING_NAME = 'NEXT_INC_CACHE_R2_BUCKET';
|
||||||
export const DEFAULT_PREFIX = 'incremental-cache';
|
export const DEFAULT_PREFIX = 'incremental-cache';
|
||||||
|
|
||||||
@@ -79,12 +81,10 @@ class GitbookIncrementalCache implements IncrementalCache {
|
|||||||
},
|
},
|
||||||
async (span) => {
|
async (span) => {
|
||||||
span.setAttribute('cacheType', cacheType ?? 'cache');
|
span.setAttribute('cacheType', cacheType ?? 'cache');
|
||||||
const r2 = getCloudflareContext().env[BINDING_NAME];
|
|
||||||
const localCache = await this.getCacheInstance();
|
const localCache = await this.getCacheInstance();
|
||||||
if (!r2) throw new Error('No R2 bucket');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await r2.put(cacheKey, JSON.stringify(value));
|
await this.writeToR2(cacheKey, JSON.stringify(value));
|
||||||
|
|
||||||
//TODO: Check if there is any places where we don't have tags
|
//TODO: Check if there is any places where we don't have tags
|
||||||
// Ideally we should always have tags, but in case we don't, we need to decide how to handle it
|
// Ideally we should always have tags, but in case we don't, we need to decide how to handle it
|
||||||
@@ -145,6 +145,22 @@ class GitbookIncrementalCache implements IncrementalCache {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async writeToR2(key: string, value: string): Promise<void> {
|
||||||
|
const env = getCloudflareContext().env as {
|
||||||
|
WRITE_BUFFER: DurableObjectNamespace<
|
||||||
|
Rpc.DurableObjectBranded & {
|
||||||
|
write: (key: string, value: string) => Promise<void>;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
const id = env.WRITE_BUFFER.idFromName(key);
|
||||||
|
|
||||||
|
// A stub is a client used to invoke methods on the Durable Object
|
||||||
|
const stub = env.WRITE_BUFFER.get(id);
|
||||||
|
|
||||||
|
await stub.write(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
async getCacheInstance(): Promise<Cache> {
|
async getCacheInstance(): Promise<Cache> {
|
||||||
if (this.localCache) return this.localCache;
|
if (this.localCache) return this.localCache;
|
||||||
this.localCache = await caches.open('incremental-cache');
|
this.localCache = await caches.open('incremental-cache');
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { trace } from '@/lib/tracing';
|
||||||
|
import type { Queue } from '@opennextjs/aws/types/overrides.js';
|
||||||
|
import { getCloudflareContext } from '@opennextjs/cloudflare';
|
||||||
|
import doQueue from '@opennextjs/cloudflare/overrides/queue/do-queue';
|
||||||
|
import memoryQueue from '@opennextjs/cloudflare/overrides/queue/memory-queue';
|
||||||
|
|
||||||
|
interface Env {
|
||||||
|
STAGE?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'GitbookISRQueue',
|
||||||
|
send: async (msg) => {
|
||||||
|
return trace({ operation: 'gitbookISRQueueSend', name: msg.MessageBody.url }, async () => {
|
||||||
|
const { ctx, env } = getCloudflareContext();
|
||||||
|
const hasDurableObject =
|
||||||
|
(env as Env).STAGE !== 'dev' && (env as Env).STAGE !== 'preview';
|
||||||
|
ctx.waitUntil(hasDurableObject ? memoryQueue.send(msg) : doQueue.send(msg));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
} satisfies Queue;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import type { Queue } from '@opennextjs/aws/types/overrides.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'GitbookISRQueue',
|
||||||
|
send: async (msg) => {
|
||||||
|
// We should never reach this point in the server. If that's the case, we should log it.
|
||||||
|
console.warn('GitbookISRQueue: send called on server side, this should not happen.', msg);
|
||||||
|
},
|
||||||
|
} satisfies Queue;
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { trace } from '@/lib/tracing';
|
||||||
|
import type { NextModeTagCache } from '@opennextjs/aws/types/overrides.js';
|
||||||
|
import doShardedTagCache from '@opennextjs/cloudflare/overrides/tag-cache/do-sharded-tag-cache';
|
||||||
|
import { softTagFilter } from '@opennextjs/cloudflare/overrides/tag-cache/tag-cache-filter';
|
||||||
|
|
||||||
|
const originalTagCache = doShardedTagCache({
|
||||||
|
baseShardSize: 12,
|
||||||
|
regionalCache: true,
|
||||||
|
// We can probably increase this value even further
|
||||||
|
regionalCacheTtlSec: 60,
|
||||||
|
shardReplication: {
|
||||||
|
numberOfSoftReplicas: 2,
|
||||||
|
numberOfHardReplicas: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'GitbookTagCache',
|
||||||
|
mode: 'nextMode',
|
||||||
|
getLastRevalidated: async (tags: string[]) => {
|
||||||
|
const tagsToCheck = tags.filter(softTagFilter);
|
||||||
|
if (tagsToCheck.length === 0) {
|
||||||
|
// If we reach here, it probably means that there is an issue that we'll need to address.
|
||||||
|
console.warn(
|
||||||
|
'getLastRevalidated - No valid tags to check for last revalidation, original tags:',
|
||||||
|
tags
|
||||||
|
);
|
||||||
|
return 0; // If no tags to check, return 0
|
||||||
|
}
|
||||||
|
return trace(
|
||||||
|
{
|
||||||
|
operation: 'gitbookTagCacheGetLastRevalidated',
|
||||||
|
name: tagsToCheck.join(', '),
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
return await originalTagCache.getLastRevalidated(tagsToCheck);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
hasBeenRevalidated: async (tags: string[], lastModified?: number) => {
|
||||||
|
const tagsToCheck = tags.filter(softTagFilter);
|
||||||
|
if (tagsToCheck.length === 0) {
|
||||||
|
// If we reach here, it probably means that there is an issue that we'll need to address.
|
||||||
|
console.warn(
|
||||||
|
'hasBeenRevalidated - No valid tags to check for revalidation, original tags:',
|
||||||
|
tags
|
||||||
|
);
|
||||||
|
return false; // If no tags to check, return false
|
||||||
|
}
|
||||||
|
return trace(
|
||||||
|
{
|
||||||
|
operation: 'gitbookTagCacheHasBeenRevalidated',
|
||||||
|
name: tagsToCheck.join(', '),
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
const result = await originalTagCache.hasBeenRevalidated(tagsToCheck, lastModified);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
writeTags: async (tags: string[]) => {
|
||||||
|
return trace(
|
||||||
|
{
|
||||||
|
operation: 'gitbookTagCacheWriteTags',
|
||||||
|
name: tags.join(', '),
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
const tagsToWrite = tags.filter(softTagFilter);
|
||||||
|
if (tagsToWrite.length === 0) {
|
||||||
|
console.warn('writeTags - No valid tags to write');
|
||||||
|
return; // If no tags to write, exit early
|
||||||
|
}
|
||||||
|
// Write only the filtered tags
|
||||||
|
await originalTagCache.writeTags(tagsToWrite);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
} satisfies NextModeTagCache;
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@gitbook/api": "^0.115.0",
|
"@gitbook/api": "^0.115.0",
|
||||||
"@gitbook/cache-tags": "workspace:*",
|
"@gitbook/cache-tags": "workspace:*",
|
||||||
"@opennextjs/cloudflare": "1.0.3",
|
"@opennextjs/cloudflare": "1.0.4",
|
||||||
"@sindresorhus/fnv1a": "^3.1.0",
|
"@sindresorhus/fnv1a": "^3.1.0",
|
||||||
"assert-never": "^1.2.1",
|
"assert-never": "^1.2.1",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
@@ -30,6 +30,8 @@
|
|||||||
"start": "next start",
|
"start": "next start",
|
||||||
"build:v2:cloudflare": "opennextjs-cloudflare build",
|
"build:v2:cloudflare": "opennextjs-cloudflare build",
|
||||||
"dev:v2:cloudflare": "wrangler dev --port 8771 --env preview",
|
"dev:v2:cloudflare": "wrangler dev --port 8771 --env preview",
|
||||||
|
"dev:v2:cf:middleware": "wrangler dev --port 8771 --inspector-port 9230 --env dev --config ./openNext/customWorkers/middlewareWrangler.jsonc",
|
||||||
|
"dev:v2:cf:server": "wrangler dev --port 8772 --env dev --config ./openNext/customWorkers/defaultWrangler.jsonc",
|
||||||
"unit": "bun test",
|
"unit": "bun test",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ export function getBaseContext(input: {
|
|||||||
// Create link in the same format for links to other sites/sections.
|
// Create link in the same format for links to other sites/sections.
|
||||||
linker.toLinkForContent = (rawURL: string) => {
|
linker.toLinkForContent = (rawURL: string) => {
|
||||||
const urlObject = new URL(rawURL);
|
const urlObject = new URL(rawURL);
|
||||||
return `/url/${urlObject.host}${urlObject.pathname}${urlObject.search}`;
|
return `/url/${urlObject.host}${urlObject.pathname}${urlObject.search}${urlObject.hash}`;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,13 @@ import { trace } from '@/lib/tracing';
|
|||||||
import {
|
import {
|
||||||
type ComputedContentSource,
|
type ComputedContentSource,
|
||||||
GitBookAPI,
|
GitBookAPI,
|
||||||
type GitBookAPIServiceBinding,
|
|
||||||
type HttpResponse,
|
type HttpResponse,
|
||||||
type RenderIntegrationUI,
|
type RenderIntegrationUI,
|
||||||
} from '@gitbook/api';
|
} from '@gitbook/api';
|
||||||
import { getCacheTag, getComputedContentSourceCacheTags } from '@gitbook/cache-tags';
|
import { getCacheTag, getComputedContentSourceCacheTags } from '@gitbook/cache-tags';
|
||||||
import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_USER_AGENT } from '@v2/lib/env';
|
import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_USER_AGENT } from '@v2/lib/env';
|
||||||
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache';
|
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache';
|
||||||
import { getCloudflareContext, getCloudflareRequestGlobal } from './cloudflare';
|
import { getCloudflareRequestGlobal } from './cloudflare';
|
||||||
import { DataFetcherError, wrapDataFetcherError } from './errors';
|
import { DataFetcherError, wrapDataFetcherError } from './errors';
|
||||||
import { withCacheKey, withoutConcurrentExecution } from './memoize';
|
import { withCacheKey, withoutConcurrentExecution } from './memoize';
|
||||||
import type { GitBookDataFetcher } from './types';
|
import type { GitBookDataFetcher } from './types';
|
||||||
@@ -828,36 +827,16 @@ async function* streamAIResponse(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let loggedServiceBinding = false;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new API client.
|
* Create a new API client.
|
||||||
*/
|
*/
|
||||||
export function apiClient(input: DataFetcherInput = { apiToken: null }) {
|
export function apiClient(input: DataFetcherInput = { apiToken: null }) {
|
||||||
const { apiToken } = input;
|
const { apiToken } = input;
|
||||||
let serviceBinding: GitBookAPIServiceBinding | undefined;
|
|
||||||
|
|
||||||
const cloudflareContext = getCloudflareContext();
|
|
||||||
if (cloudflareContext) {
|
|
||||||
// @ts-expect-error
|
|
||||||
serviceBinding = cloudflareContext.env.GITBOOK_API as GitBookAPIServiceBinding | undefined;
|
|
||||||
if (!loggedServiceBinding) {
|
|
||||||
loggedServiceBinding = true;
|
|
||||||
if (serviceBinding) {
|
|
||||||
// biome-ignore lint/suspicious/noConsole: we want to log here
|
|
||||||
console.log(`using service binding for the API (${GITBOOK_API_URL})`);
|
|
||||||
} else {
|
|
||||||
// biome-ignore lint/suspicious/noConsole: we want to log here
|
|
||||||
console.warn(`no service binding for the API (${GITBOOK_API_URL})`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const api = new GitBookAPI({
|
const api = new GitBookAPI({
|
||||||
authToken: apiToken || GITBOOK_API_TOKEN || undefined,
|
authToken: apiToken || GITBOOK_API_TOKEN || undefined,
|
||||||
endpoint: GITBOOK_API_URL,
|
endpoint: GITBOOK_API_URL,
|
||||||
userAgent: GITBOOK_USER_AGENT,
|
userAgent: GITBOOK_USER_AGENT,
|
||||||
serviceBinding,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return api;
|
return api;
|
||||||
|
|||||||
@@ -4,5 +4,4 @@ export * from './pages';
|
|||||||
export * from './urls';
|
export * from './urls';
|
||||||
export * from './errors';
|
export * from './errors';
|
||||||
export * from './lookup';
|
export * from './lookup';
|
||||||
export * from './proxy';
|
|
||||||
export * from './visitor';
|
export * from './visitor';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { withLeadingSlash, withTrailingSlash } from '@/lib/paths';
|
import { withLeadingSlash, withTrailingSlash } from '@/lib/paths';
|
||||||
import type { PublishedSiteContent } from '@gitbook/api';
|
import type { PublishedSiteContent } from '@gitbook/api';
|
||||||
import { getProxyRequestIdentifier, isProxyRequest } from './proxy';
|
import { getProxyRequestIdentifier, isProxyRequest } from '@v2/lib/proxy';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the appropriate base path for the visitor authentication cookie.
|
* Get the appropriate base path for the visitor authentication cookie.
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { describe, expect, it } from 'bun:test';
|
||||||
|
import { getImageResizingContextId } from './getImageResizingContextId';
|
||||||
|
|
||||||
|
describe('getImageResizingContextId', () => {
|
||||||
|
it('should return proxy identifier for proxy requests', () => {
|
||||||
|
const proxyRequestURL = new URL('https://proxy.gitbook.site/sites/site_foo/hello/world');
|
||||||
|
expect(getImageResizingContextId(proxyRequestURL)).toBe('sites/site_foo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return preview identifier for preview requests', () => {
|
||||||
|
const previewRequestURL = new URL('https://preview/site_foo/hello/world');
|
||||||
|
expect(getImageResizingContextId(previewRequestURL)).toBe('site_foo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return host for regular requests', () => {
|
||||||
|
const regularRequestURL = new URL('https://example.com/docs/foo/hello/world');
|
||||||
|
expect(getImageResizingContextId(regularRequestURL)).toBe('example.com');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { getProxyRequestIdentifier, isProxyRequest } from '../data';
|
import { getPreviewRequestIdentifier, isPreviewRequest } from '@v2/lib/preview';
|
||||||
|
import { getProxyRequestIdentifier, isProxyRequest } from '@v2/lib/proxy';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the site identifier to use for image resizing for an incoming request.
|
* Get the site identifier to use for image resizing for an incoming request.
|
||||||
@@ -8,6 +9,9 @@ export function getImageResizingContextId(url: URL): string {
|
|||||||
if (isProxyRequest(url)) {
|
if (isProxyRequest(url)) {
|
||||||
return getProxyRequestIdentifier(url);
|
return getProxyRequestIdentifier(url);
|
||||||
}
|
}
|
||||||
|
if (isPreviewRequest(url)) {
|
||||||
|
return getPreviewRequestIdentifier(url);
|
||||||
|
}
|
||||||
|
|
||||||
return url.host;
|
return url.host;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const siteGitBookIO = createLinker({
|
|||||||
siteBasePath: '/sitename/',
|
siteBasePath: '/sitename/',
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('toPathInContent', () => {
|
describe('toPathInSpace', () => {
|
||||||
it('should return the correct path', () => {
|
it('should return the correct path', () => {
|
||||||
expect(root.toPathInSpace('some/path')).toBe('/some/path');
|
expect(root.toPathInSpace('some/path')).toBe('/some/path');
|
||||||
expect(variantInSection.toPathInSpace('some/path')).toBe('/section/variant/some/path');
|
expect(variantInSection.toPathInSpace('some/path')).toBe('/section/variant/some/path');
|
||||||
@@ -29,6 +29,16 @@ describe('toPathInContent', () => {
|
|||||||
expect(root.toPathInSpace('/some/path')).toBe('/some/path');
|
expect(root.toPathInSpace('/some/path')).toBe('/some/path');
|
||||||
expect(variantInSection.toPathInSpace('/some/path')).toBe('/section/variant/some/path');
|
expect(variantInSection.toPathInSpace('/some/path')).toBe('/section/variant/some/path');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should remove the trailing slash', () => {
|
||||||
|
expect(root.toPathInSpace('some/path/')).toBe('/some/path');
|
||||||
|
expect(variantInSection.toPathInSpace('some/path/')).toBe('/section/variant/some/path');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not add a trailing slash', () => {
|
||||||
|
expect(root.toPathInSpace('')).toBe('');
|
||||||
|
expect(variantInSection.toPathInSpace('')).toBe('/section/variant');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('toPathInSite', () => {
|
describe('toPathInSite', () => {
|
||||||
@@ -36,6 +46,16 @@ describe('toPathInSite', () => {
|
|||||||
expect(root.toPathInSite('some/path')).toBe('/some/path');
|
expect(root.toPathInSite('some/path')).toBe('/some/path');
|
||||||
expect(siteGitBookIO.toPathInSite('some/path')).toBe('/sitename/some/path');
|
expect(siteGitBookIO.toPathInSite('some/path')).toBe('/sitename/some/path');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should remove the trailing slash', () => {
|
||||||
|
expect(root.toPathInSite('some/path/')).toBe('/some/path');
|
||||||
|
expect(siteGitBookIO.toPathInSite('some/path/')).toBe('/sitename/some/path');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not add a trailing slash', () => {
|
||||||
|
expect(root.toPathInSite('')).toBe('');
|
||||||
|
expect(siteGitBookIO.toPathInSite('')).toBe('/sitename');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('toRelativePathInSite', () => {
|
describe('toRelativePathInSite', () => {
|
||||||
|
|||||||
@@ -128,5 +128,9 @@ export function createLinker(
|
|||||||
function joinPaths(prefix: string, path: string): string {
|
function joinPaths(prefix: string, path: string): string {
|
||||||
const prefixPath = prefix.endsWith('/') ? prefix : `${prefix}/`;
|
const prefixPath = prefix.endsWith('/') ? prefix : `${prefix}/`;
|
||||||
const suffixPath = path.startsWith('/') ? path.slice(1) : path;
|
const suffixPath = path.startsWith('/') ? path.slice(1) : path;
|
||||||
return prefixPath + suffixPath;
|
return removeTrailingSlash(prefixPath + suffixPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeTrailingSlash(path: string): string {
|
||||||
|
return path.endsWith('/') ? path.slice(0, -1) : path;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { describe, expect, it } from 'bun:test';
|
||||||
|
import { getPreviewRequestIdentifier, isPreviewRequest } from './preview';
|
||||||
|
|
||||||
|
describe('isPreviewRequest', () => {
|
||||||
|
it('should return true for preview requests', () => {
|
||||||
|
const previewRequestURL = new URL('https://preview/site_foo/hello/world');
|
||||||
|
expect(isPreviewRequest(previewRequestURL)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for non-preview requests', () => {
|
||||||
|
const nonPreviewRequestURL = new URL('https://example.com/docs/foo/hello/world');
|
||||||
|
expect(isPreviewRequest(nonPreviewRequestURL)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getPreviewRequestIdentifier', () => {
|
||||||
|
it('should return the correct identifier for preview requests', () => {
|
||||||
|
const previewRequestURL = new URL('https://preview/site_foo/hello/world');
|
||||||
|
expect(getPreviewRequestIdentifier(previewRequestURL)).toBe('site_foo');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Check if the request to the site is a preview request.
|
||||||
|
*/
|
||||||
|
export function isPreviewRequest(requestURL: URL): boolean {
|
||||||
|
return requestURL.host === 'preview';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPreviewRequestIdentifier(requestURL: URL): string {
|
||||||
|
// For preview requests, we extract the site ID from the pathname
|
||||||
|
// e.g. https://preview/site_id/...
|
||||||
|
const pathname = requestURL.pathname.slice(1).split('/');
|
||||||
|
return pathname[0];
|
||||||
|
}
|
||||||
@@ -104,6 +104,7 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
|
|||||||
const resolve = ADAPTIVE_CONTENT_HOSTS.includes(siteRequestURL.hostname)
|
const resolve = ADAPTIVE_CONTENT_HOSTS.includes(siteRequestURL.hostname)
|
||||||
? resolvePublishedContentByUrl
|
? resolvePublishedContentByUrl
|
||||||
: getPublishedContentByURL;
|
: getPublishedContentByURL;
|
||||||
|
|
||||||
const siteURLData = await throwIfDataError(
|
const siteURLData = await throwIfDataError(
|
||||||
resolve({
|
resolve({
|
||||||
url: siteRequestURL.toString(),
|
url: siteRequestURL.toString(),
|
||||||
@@ -290,7 +291,7 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
|
|||||||
return writeResponseCookies(response, cookies);
|
return writeResponseCookies(response, cookies);
|
||||||
};
|
};
|
||||||
|
|
||||||
// For https://preview/<siteURL> requests,
|
// For https://preview/<siteId> requests,
|
||||||
if (siteRequestURL.hostname === 'preview') {
|
if (siteRequestURL.hostname === 'preview') {
|
||||||
return serveWithQueryAPIToken(
|
return serveWithQueryAPIToken(
|
||||||
// We scope the API token to the site ID.
|
// We scope the API token to the site ID.
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import crypto from 'node:crypto';
|
||||||
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
|
||||||
|
interface JsonBody {
|
||||||
|
// The paths need to be the rewritten one, `res.revalidate` call don't go through the middleware
|
||||||
|
paths: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
// Only allow POST requests
|
||||||
|
if (req.method !== 'POST') {
|
||||||
|
return res.status(405).json({ error: 'Method not allowed' });
|
||||||
|
}
|
||||||
|
const signatureHeader = req.headers['x-gitbook-signature'] as string | undefined;
|
||||||
|
if (!signatureHeader) {
|
||||||
|
return res.status(400).json({ error: 'Missing signature header' });
|
||||||
|
}
|
||||||
|
// We cannot use env from `@/v2/lib/env` here as it make it crash because of the import "server-only" in the file.
|
||||||
|
if (process.env.GITBOOK_SECRET) {
|
||||||
|
try {
|
||||||
|
const computedSignature = crypto
|
||||||
|
.createHmac('sha256', process.env.GITBOOK_SECRET)
|
||||||
|
.update(JSON.stringify(req.body))
|
||||||
|
.digest('hex');
|
||||||
|
|
||||||
|
if (computedSignature === signatureHeader) {
|
||||||
|
const results = await Promise.allSettled(
|
||||||
|
(req.body as JsonBody).paths.map((path) => {
|
||||||
|
// biome-ignore lint/suspicious/noConsole: we want to log here
|
||||||
|
console.log(`Revalidating path: ${path}`);
|
||||||
|
return res.revalidate(path);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return res.status(200).json({
|
||||||
|
success: results.every((result) => result.status === 'fulfilled'),
|
||||||
|
errors: results
|
||||||
|
.filter((result) => result.status === 'rejected')
|
||||||
|
.map((result) => (result as PromiseRejectedResult).reason),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return res.status(401).json({ error: 'Invalid signature' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during revalidation:', error);
|
||||||
|
return res.status(400).json({ error: 'Invalid request or unable to parse JSON' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If no secret is set, we do not allow revalidation
|
||||||
|
return res.status(403).json({ error: 'Revalidation is disabled' });
|
||||||
|
}
|
||||||
@@ -15,10 +15,14 @@
|
|||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
"vars": {
|
"vars": {
|
||||||
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true"
|
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true",
|
||||||
|
"IS_PREVIEW": "false"
|
||||||
},
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"preview": {
|
"preview": {
|
||||||
|
"vars": {
|
||||||
|
"IS_PREVIEW": "true"
|
||||||
|
},
|
||||||
"r2_buckets": [
|
"r2_buckets": [
|
||||||
{
|
{
|
||||||
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||||
@@ -91,9 +95,12 @@
|
|||||||
},
|
},
|
||||||
"production": {
|
"production": {
|
||||||
"vars": {
|
"vars": {
|
||||||
// This is a bit misleading, but it means that we can have 100 concurrent revalidations
|
// This is a bit misleading, but it means that we can have 500 concurrent revalidations
|
||||||
// This means that we'll have up to 20 durable objects instance running at the same time
|
// This means that we'll have up to 100 durable objects instance running at the same time
|
||||||
"MAX_REVALIDATE_CONCURRENCY": "20"
|
"MAX_REVALIDATE_CONCURRENCY": "100",
|
||||||
|
// Temporary variable to find the issue once deployed
|
||||||
|
// TODO: remove this once the issue is fixed
|
||||||
|
"DEBUG_CLOUDFLARE": "true"
|
||||||
},
|
},
|
||||||
"routes": [
|
"routes": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,35 +18,18 @@ export function AnnouncementBanner(props: {
|
|||||||
}) {
|
}) {
|
||||||
const { announcement, contentRef } = props;
|
const { announcement, contentRef } = props;
|
||||||
|
|
||||||
const hasLink = announcement.link && contentRef?.href;
|
const hasLink = contentRef?.href;
|
||||||
const closeable = announcement.style !== 'danger';
|
const closeable = announcement.style !== 'danger';
|
||||||
|
|
||||||
const Tag = hasLink ? Link : 'div';
|
|
||||||
const style = BANNER_STYLES[announcement.style];
|
const style = BANNER_STYLES[announcement.style];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="announcement-banner theme-bold:bg-header-background pt-4 pb-2">
|
<div id="announcement-banner" className="theme-bold:bg-header-background pt-4 pb-2">
|
||||||
<div className="scroll-nojump">
|
<div className="scroll-nojump">
|
||||||
<div className={tcls('relative', CONTAINER_STYLE)}>
|
<div className={tcls('relative', CONTAINER_STYLE)}>
|
||||||
<Tag
|
<AnnouncementBannerParent
|
||||||
href={contentRef?.href ?? ''}
|
announcement={announcement}
|
||||||
className={tcls(
|
closeable={closeable}
|
||||||
'flex w-full items-start justify-center overflow-hidden rounded-md straight-corners:rounded-none px-4 py-3 text-neutral-strong text-sm theme-bold:ring-1 theme-gradient:ring-1 ring-inset transition-colors',
|
contentRef={contentRef}
|
||||||
style.container,
|
|
||||||
closeable && 'pr-12',
|
|
||||||
hasLink && style.hover
|
|
||||||
)}
|
|
||||||
insights={
|
|
||||||
announcement.link
|
|
||||||
? {
|
|
||||||
type: 'link_click',
|
|
||||||
link: {
|
|
||||||
target: announcement.link.to,
|
|
||||||
position: SiteInsightsLinkPosition.Announcement,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
icon={style.icon as IconName}
|
icon={style.icon as IconName}
|
||||||
@@ -75,7 +58,7 @@ export function AnnouncementBanner(props: {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</Tag>
|
</AnnouncementBannerParent>
|
||||||
{closeable ? (
|
{closeable ? (
|
||||||
<button
|
<button
|
||||||
className={`absolute top-0 right-4 mt-2 mr-2 rounded straight-corners:rounded-none p-1.5 transition-all hover:ring-1 sm:right-6 md:right-8 ${style.close}`}
|
className={`absolute top-0 right-4 mt-2 mr-2 rounded straight-corners:rounded-none p-1.5 transition-all hover:ring-1 sm:right-6 md:right-8 ${style.close}`}
|
||||||
@@ -91,6 +74,48 @@ export function AnnouncementBanner(props: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the appropriate parent for the announcement banner depending on the presence of a link.
|
||||||
|
*/
|
||||||
|
function AnnouncementBannerParent(props: {
|
||||||
|
announcement: CustomizationAnnouncement;
|
||||||
|
children: React.ReactNode;
|
||||||
|
closeable: boolean;
|
||||||
|
contentRef: ResolvedContentRef | null;
|
||||||
|
}) {
|
||||||
|
const { announcement, contentRef, closeable, children } = props;
|
||||||
|
const style = BANNER_STYLES[announcement.style];
|
||||||
|
|
||||||
|
const classNames = [
|
||||||
|
'flex w-full items-start justify-center overflow-hidden rounded-md straight-corners:rounded-none px-4 py-3 text-neutral-strong text-sm theme-bold:ring-1 theme-gradient:ring-1 ring-inset transition-colors',
|
||||||
|
style.container,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (contentRef?.href) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={contentRef.href}
|
||||||
|
className={tcls(classNames, closeable && 'pr-12', style.hover)}
|
||||||
|
insights={
|
||||||
|
announcement.link
|
||||||
|
? {
|
||||||
|
type: 'link_click',
|
||||||
|
link: {
|
||||||
|
target: announcement.link.to,
|
||||||
|
position: SiteInsightsLinkPosition.Announcement,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className={tcls(classNames)}>{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dismiss the announcement banner and store the dismissal state in local storage.
|
* Dismiss the announcement banner and store the dismissal state in local storage.
|
||||||
* @see AnnouncementScript
|
* @see AnnouncementScript
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
SiteInsightsLinkPosition,
|
SiteInsightsLinkPosition,
|
||||||
} from '@gitbook/api';
|
} from '@gitbook/api';
|
||||||
|
|
||||||
import { Link } from '@/components/primitives';
|
import { LinkBox, LinkOverlay } from '@/components/primitives';
|
||||||
import { Image } from '@/components/utils';
|
import { Image } from '@/components/utils';
|
||||||
import { resolveContentRef } from '@/lib/references';
|
import { resolveContentRef } from '@/lib/references';
|
||||||
import { type ClassValue, tcls } from '@/lib/tailwind';
|
import { type ClassValue, tcls } from '@/lib/tailwind';
|
||||||
@@ -44,7 +44,6 @@ export async function RecordCard(
|
|||||||
<div
|
<div
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'grid-area-1-1',
|
'grid-area-1-1',
|
||||||
'z-0',
|
|
||||||
'relative',
|
'relative',
|
||||||
'grid',
|
'grid',
|
||||||
'bg-tint-base',
|
'bg-tint-base',
|
||||||
@@ -151,7 +150,6 @@ export async function RecordCard(
|
|||||||
'rounded-md',
|
'rounded-md',
|
||||||
'straight-corners:rounded-none',
|
'straight-corners:rounded-none',
|
||||||
'dark:shadow-transparent',
|
'dark:shadow-transparent',
|
||||||
'z-0',
|
|
||||||
|
|
||||||
'before:pointer-events-none',
|
'before:pointer-events-none',
|
||||||
'before:grid-area-1-1',
|
'before:grid-area-1-1',
|
||||||
@@ -167,19 +165,22 @@ export async function RecordCard(
|
|||||||
|
|
||||||
if (target && targetRef) {
|
if (target && targetRef) {
|
||||||
return (
|
return (
|
||||||
<Link
|
// We don't use `Link` directly here because we could end up in a situation where
|
||||||
href={target.href}
|
// a link is rendered inside a link, which is not allowed in HTML.
|
||||||
className={tcls(style, 'hover:before:ring-tint-12/5')}
|
// It causes an hydration error in React.
|
||||||
insights={{
|
<LinkBox href={target.href} className={tcls(style, 'hover:before:ring-tint-12/5')}>
|
||||||
type: 'link_click',
|
<LinkOverlay
|
||||||
link: {
|
href={target.href}
|
||||||
target: targetRef,
|
insights={{
|
||||||
position: SiteInsightsLinkPosition.Content,
|
type: 'link_click',
|
||||||
},
|
link: {
|
||||||
}}
|
target: targetRef,
|
||||||
>
|
position: SiteInsightsLinkPosition.Content,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{body}
|
{body}
|
||||||
</Link>
|
</LinkBox>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -115,7 +115,8 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
|||||||
return <Tag className={tcls(['w-full', verticalAlignment])}>{''}</Tag>;
|
return <Tag className={tcls(['w-full', verticalAlignment])}>{''}</Tag>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const horizontalAlignment = `[&_*]:${getColumnAlignment(definition)} ${getColumnAlignment(definition)}`;
|
const horizontalAlignment = getColumnAlignment(definition);
|
||||||
|
const childrenHorizontalAlignment = `[&_*]:${horizontalAlignment}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Blocks
|
<Blocks
|
||||||
@@ -131,6 +132,7 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
|||||||
'leading-normal',
|
'leading-normal',
|
||||||
verticalAlignment,
|
verticalAlignment,
|
||||||
horizontalAlignment,
|
horizontalAlignment,
|
||||||
|
childrenHorizontalAlignment,
|
||||||
]}
|
]}
|
||||||
context={context}
|
context={context}
|
||||||
blockStyle={['w-full', 'max-w-[unset]']}
|
blockStyle={['w-full', 'max-w-[unset]']}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export function Footer(props: { context: GitBookSiteContext }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<footer
|
<footer
|
||||||
|
id="site-footer"
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'border-tint-subtle border-t',
|
'border-tint-subtle border-t',
|
||||||
// If the footer only contains a mode toggle, we only show it on smaller screens
|
// If the footer only contains a mode toggle, we only show it on smaller screens
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { SiteSpace } from '@gitbook/api';
|
|||||||
|
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
|
|
||||||
|
import { joinPath } from '@/lib/paths';
|
||||||
import type { GitBookSiteContext } from '@v2/lib/context';
|
import type { GitBookSiteContext } from '@v2/lib/context';
|
||||||
import { DropdownChevron, DropdownMenu } from './DropdownMenu';
|
import { DropdownChevron, DropdownMenu } from './DropdownMenu';
|
||||||
import { SpacesDropdownMenuItem } from './SpacesDropdownMenuItem';
|
import { SpacesDropdownMenuItem } from './SpacesDropdownMenuItem';
|
||||||
@@ -74,7 +75,7 @@ export function SpacesDropdown(props: {
|
|||||||
title: otherSiteSpace.title,
|
title: otherSiteSpace.title,
|
||||||
url: otherSiteSpace.urls.published
|
url: otherSiteSpace.urls.published
|
||||||
? linker.toLinkForContent(otherSiteSpace.urls.published)
|
? linker.toLinkForContent(otherSiteSpace.urls.published)
|
||||||
: otherSiteSpace.space.urls.app,
|
: getFallbackSiteSpaceURL(otherSiteSpace, context),
|
||||||
}}
|
}}
|
||||||
active={otherSiteSpace.id === siteSpace.id}
|
active={otherSiteSpace.id === siteSpace.id}
|
||||||
/>
|
/>
|
||||||
@@ -82,3 +83,14 @@ export function SpacesDropdown(props: {
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the site is not published yet, `urls.published` is not available.
|
||||||
|
* To ensure navigation works in preview, we compute a relative URL from the siteSpace path.
|
||||||
|
*/
|
||||||
|
function getFallbackSiteSpaceURL(siteSpace: SiteSpace, context: GitBookSiteContext) {
|
||||||
|
const { linker, sections } = context;
|
||||||
|
return linker.toPathInSite(
|
||||||
|
sections?.current ? joinPath(sections.current.path, siteSpace.path) : siteSpace.path
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export async function CustomizationRootLayout(props: {
|
|||||||
lang={customization.internationalization.locale}
|
lang={customization.internationalization.locale}
|
||||||
className={tcls(
|
className={tcls(
|
||||||
customization.header.preset === CustomizationHeaderPreset.None
|
customization.header.preset === CustomizationHeaderPreset.None
|
||||||
? 'site-header-none'
|
? null
|
||||||
: 'scroll-pt-[76px]', // Take the sticky header in consideration for the scrolling
|
: 'scroll-pt-[76px]', // Take the sticky header in consideration for the scrolling
|
||||||
customization.styling.corners === CustomizationCorners.Straight
|
customization.styling.corners === CustomizationCorners.Straight
|
||||||
? ' straight-corners'
|
? ' straight-corners'
|
||||||
|
|||||||
@@ -148,6 +148,13 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.elevate-link {
|
||||||
|
& a[href]:not(.link-overlay) {
|
||||||
|
position: relative;
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
@@ -161,6 +168,6 @@ html.dark {
|
|||||||
color-scheme: dark light;
|
color-scheme: dark light;
|
||||||
}
|
}
|
||||||
|
|
||||||
html.announcement-hidden .announcement-banner {
|
html.announcement-hidden #announcement-banner {
|
||||||
@apply hidden;
|
@apply hidden;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ function encodeSection(context: GitBookSiteContext, section: SiteSection) {
|
|||||||
description: section.description,
|
description: section.description,
|
||||||
icon: section.icon,
|
icon: section.icon,
|
||||||
object: section.object,
|
object: section.object,
|
||||||
url: section.urls.published ? linker.toLinkForContent(section.urls.published) : '',
|
url: section.urls.published
|
||||||
|
? linker.toLinkForContent(section.urls.published)
|
||||||
|
: linker.toPathInSite(section.path),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,11 @@ export async function PageDocumentItem(props: {
|
|||||||
context: GitBookSiteContext;
|
context: GitBookSiteContext;
|
||||||
}) {
|
}) {
|
||||||
const { rootPages, page, context } = props;
|
const { rootPages, page, context } = props;
|
||||||
const href = context.linker.toPathForPage({ pages: rootPages, page });
|
let href = context.linker.toPathForPage({ pages: rootPages, page });
|
||||||
|
// toPathForPage can returns an empty path, this will cause all links to point to the current page.
|
||||||
|
if (href === '') {
|
||||||
|
href = '/';
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className="flex flex-col">
|
<li className="flex flex-col">
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { tcls } from '@/lib/tailwind';
|
|||||||
|
|
||||||
import { PagesList } from './PagesList';
|
import { PagesList } from './PagesList';
|
||||||
import { TOCScrollContainer } from './TOCScroller';
|
import { TOCScrollContainer } from './TOCScroller';
|
||||||
|
import { TableOfContentsScript } from './TableOfContentsScript';
|
||||||
import { Trademark } from './Trademark';
|
import { Trademark } from './Trademark';
|
||||||
|
|
||||||
export function TableOfContents(props: {
|
export function TableOfContents(props: {
|
||||||
@@ -17,97 +18,107 @@ export function TableOfContents(props: {
|
|||||||
const { space, customization, pages } = context;
|
const { space, customization, pages } = context;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside // Sidebar container, responsible for setting the right dimensions and position for the sidebar.
|
<>
|
||||||
data-testid="table-of-contents"
|
<aside // Sidebar container, responsible for setting the right dimensions and position for the sidebar.
|
||||||
className={tcls(
|
data-testid="table-of-contents"
|
||||||
'group',
|
id="table-of-contents"
|
||||||
'text-sm',
|
|
||||||
|
|
||||||
'grow-0',
|
|
||||||
'shrink-0',
|
|
||||||
'basis-full',
|
|
||||||
'lg:basis-72',
|
|
||||||
'page-no-toc:lg:basis-56',
|
|
||||||
|
|
||||||
'relative',
|
|
||||||
'z-[1]',
|
|
||||||
'lg:sticky',
|
|
||||||
// Without header
|
|
||||||
'lg:top-0',
|
|
||||||
'lg:h-screen',
|
|
||||||
|
|
||||||
// With header
|
|
||||||
'site-header:lg:top-16',
|
|
||||||
'site-header:lg:h-[calc(100vh_-_4rem)]',
|
|
||||||
|
|
||||||
// With header and sections
|
|
||||||
'site-header-sections:lg:top-[6.75rem]',
|
|
||||||
'site-header-sections:lg:h-[calc(100vh_-_6.75rem)]',
|
|
||||||
|
|
||||||
'pt-6',
|
|
||||||
'pb-4',
|
|
||||||
'sidebar-filled:lg:pr-6',
|
|
||||||
'page-no-toc:lg:pr-0',
|
|
||||||
|
|
||||||
'hidden',
|
|
||||||
'navigation-open:!flex',
|
|
||||||
'lg:flex',
|
|
||||||
'page-no-toc:lg:hidden',
|
|
||||||
'page-no-toc:xl:flex',
|
|
||||||
'site-header-none:page-no-toc:lg:flex',
|
|
||||||
'flex-col',
|
|
||||||
'gap-4',
|
|
||||||
|
|
||||||
'navigation-open:border-b',
|
|
||||||
'border-tint-subtle'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{header && header}
|
|
||||||
<div // The actual sidebar, either shown with a filled bg or transparent.
|
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'lg:-ms-5',
|
'group',
|
||||||
'relative flex flex-grow flex-col overflow-hidden border-tint-subtle',
|
'text-sm',
|
||||||
|
|
||||||
'sidebar-filled:bg-tint-subtle',
|
'grow-0',
|
||||||
'theme-muted:bg-tint-subtle',
|
'shrink-0',
|
||||||
'[html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle',
|
'basis-full',
|
||||||
'[html.sidebar-filled.theme-muted_&]:bg-tint-base',
|
'lg:basis-72',
|
||||||
'[html.sidebar-filled.theme-bold.tint_&]:bg-tint-base',
|
'page-no-toc:lg:basis-56',
|
||||||
'[html.sidebar-filled.theme-gradient_&]:border',
|
|
||||||
'page-no-toc:!bg-transparent',
|
|
||||||
|
|
||||||
'sidebar-filled:rounded-xl',
|
'relative',
|
||||||
'straight-corners:rounded-none'
|
'z-[1]',
|
||||||
|
'lg:sticky',
|
||||||
|
|
||||||
|
// Server-side static positioning
|
||||||
|
'lg:top-0',
|
||||||
|
'lg:h-screen',
|
||||||
|
'announcement:lg:h-[calc(100vh-4.25rem)]',
|
||||||
|
|
||||||
|
'site-header:lg:top-16',
|
||||||
|
'site-header:lg:h-[calc(100vh-4rem)]',
|
||||||
|
'announcement:site-header:lg:h-[calc(100vh-4rem-4.25rem)]',
|
||||||
|
|
||||||
|
'site-header-sections:lg:top-[6.75rem]',
|
||||||
|
'site-header-sections:lg:h-[calc(100vh-6.75rem)]',
|
||||||
|
'announcement:site-header-sections:lg:h-[calc(100vh-6.75rem-4.25rem)]',
|
||||||
|
|
||||||
|
// Client-side dynamic positioning (CSS vars applied by script)
|
||||||
|
'[html[style*="--toc-top-offset"]_&]:lg:!top-[var(--toc-top-offset)]',
|
||||||
|
'[html[style*="--toc-height"]_&]:lg:!h-[var(--toc-height)]',
|
||||||
|
|
||||||
|
'pt-6',
|
||||||
|
'pb-4',
|
||||||
|
'sidebar-filled:lg:pr-6',
|
||||||
|
'page-no-toc:lg:pr-0',
|
||||||
|
|
||||||
|
'hidden',
|
||||||
|
'navigation-open:!flex',
|
||||||
|
'lg:flex',
|
||||||
|
'page-no-toc:lg:hidden',
|
||||||
|
'page-no-toc:xl:flex',
|
||||||
|
'site-header-none:page-no-toc:lg:flex',
|
||||||
|
'flex-col',
|
||||||
|
'gap-4',
|
||||||
|
|
||||||
|
'navigation-open:border-b',
|
||||||
|
'border-tint-subtle'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{innerHeader && <div className="px-5 *:my-4">{innerHeader}</div>}
|
{header && header}
|
||||||
<TOCScrollContainer // The scrollview inside the sidebar
|
<div // The actual sidebar, either shown with a filled bg or transparent.
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'flex flex-grow flex-col p-2',
|
'lg:-ms-5',
|
||||||
customization.trademark.enabled && 'lg:pb-20',
|
'relative flex flex-grow flex-col overflow-hidden border-tint-subtle',
|
||||||
'lg:gutter-stable overflow-y-auto',
|
|
||||||
'[&::-webkit-scrollbar]:bg-transparent',
|
'sidebar-filled:bg-tint-subtle',
|
||||||
'[&::-webkit-scrollbar-thumb]:bg-transparent',
|
'theme-muted:bg-tint-subtle',
|
||||||
'group-hover:[&::-webkit-scrollbar]:bg-tint-subtle',
|
'[html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle',
|
||||||
'group-hover:[&::-webkit-scrollbar-thumb]:bg-tint-7',
|
'[html.sidebar-filled.theme-muted_&]:bg-tint-base',
|
||||||
'group-hover:[&::-webkit-scrollbar-thumb:hover]:bg-tint-8'
|
'[html.sidebar-filled.theme-bold.tint_&]:bg-tint-base',
|
||||||
|
'[html.sidebar-filled.theme-gradient_&]:border',
|
||||||
|
'page-no-toc:!bg-transparent',
|
||||||
|
|
||||||
|
'sidebar-filled:rounded-xl',
|
||||||
|
'straight-corners:rounded-none'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<PagesList
|
{innerHeader && <div className="px-5 *:my-4">{innerHeader}</div>}
|
||||||
rootPages={pages}
|
<TOCScrollContainer // The scrollview inside the sidebar
|
||||||
pages={pages}
|
className={tcls(
|
||||||
context={context}
|
'flex flex-grow flex-col p-2',
|
||||||
style="page-no-toc:hidden border-tint-subtle sidebar-list-line:border-l"
|
customization.trademark.enabled && 'lg:pb-20',
|
||||||
/>
|
'lg:gutter-stable overflow-y-auto',
|
||||||
{customization.trademark.enabled ? (
|
'[&::-webkit-scrollbar]:bg-transparent',
|
||||||
<Trademark
|
'[&::-webkit-scrollbar-thumb]:bg-transparent',
|
||||||
space={space}
|
'group-hover:[&::-webkit-scrollbar]:bg-tint-subtle',
|
||||||
customization={customization}
|
'group-hover:[&::-webkit-scrollbar-thumb]:bg-tint-7',
|
||||||
placement={SiteInsightsTrademarkPlacement.Sidebar}
|
'group-hover:[&::-webkit-scrollbar-thumb:hover]:bg-tint-8'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<PagesList
|
||||||
|
rootPages={pages}
|
||||||
|
pages={pages}
|
||||||
|
context={context}
|
||||||
|
style="page-no-toc:hidden border-tint-subtle sidebar-list-line:border-l"
|
||||||
/>
|
/>
|
||||||
) : null}
|
{customization.trademark.enabled ? (
|
||||||
</TOCScrollContainer>
|
<Trademark
|
||||||
</div>
|
space={space}
|
||||||
</aside>
|
customization={customization}
|
||||||
|
placement={SiteInsightsTrademarkPlacement.Sidebar}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</TOCScrollContainer>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
<TableOfContentsScript />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adjusts TableOfContents height based on visible elements
|
||||||
|
*/
|
||||||
|
export function TableOfContentsScript() {
|
||||||
|
useEffect(() => {
|
||||||
|
const root = document.documentElement;
|
||||||
|
|
||||||
|
// Calculate and set TOC dimensions
|
||||||
|
const updateTocLayout = () => {
|
||||||
|
// Get key elements
|
||||||
|
const header = document.getElementById('site-header');
|
||||||
|
const banner = document.getElementById('announcement-banner');
|
||||||
|
const footer = document.getElementById('site-footer');
|
||||||
|
|
||||||
|
// Set sticky top position based on header
|
||||||
|
const headerHeight = header?.offsetHeight ?? 0;
|
||||||
|
root.style.setProperty('--toc-top-offset', `${headerHeight}px`);
|
||||||
|
|
||||||
|
// Start with full viewport height minus header
|
||||||
|
let height = window.innerHeight - headerHeight;
|
||||||
|
|
||||||
|
// Subtract visible banner (if any)
|
||||||
|
if (banner && window.getComputedStyle(banner).display !== 'none') {
|
||||||
|
const bannerRect = banner.getBoundingClientRect();
|
||||||
|
if (bannerRect.height > 0 && bannerRect.bottom > 0) {
|
||||||
|
height -= Math.min(bannerRect.height, bannerRect.bottom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subtract visible footer (if any)
|
||||||
|
if (footer) {
|
||||||
|
const footerRect = footer.getBoundingClientRect();
|
||||||
|
if (footerRect.top < window.innerHeight) {
|
||||||
|
height -= Math.min(footerRect.height, window.innerHeight - footerRect.top);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update height
|
||||||
|
root.style.setProperty('--toc-height', `${height}px`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initial update
|
||||||
|
updateTocLayout();
|
||||||
|
|
||||||
|
// Let the browser handle scroll throttling naturally
|
||||||
|
window.addEventListener('scroll', updateTocLayout, { passive: true });
|
||||||
|
window.addEventListener('resize', updateTocLayout, { passive: true });
|
||||||
|
|
||||||
|
// Use MutationObserver for DOM changes
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
requestAnimationFrame(updateTocLayout);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Only observe what matters
|
||||||
|
observer.observe(document.documentElement, {
|
||||||
|
subtree: true,
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['style', 'class'],
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
window.removeEventListener('scroll', updateTocLayout);
|
||||||
|
window.removeEventListener('resize', updateTocLayout);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -1 +1,4 @@
|
|||||||
export * from './TableOfContents';
|
export { TableOfContents } from './TableOfContents';
|
||||||
|
export { PagesList } from './PagesList';
|
||||||
|
export { TOCScrollContainer } from './TOCScroller';
|
||||||
|
export { Trademark } from './Trademark';
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import NextLink, { type LinkProps as NextLinkProps } from 'next/link';
|
import NextLink, { type LinkProps as NextLinkProps } from 'next/link';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { tcls } from '@/lib/tailwind';
|
||||||
import { type TrackEventInput, useTrackEvent } from '../Insights';
|
import { type TrackEventInput, useTrackEvent } from '../Insights';
|
||||||
|
|
||||||
// Props from Next, which includes NextLinkProps and all the things anchor elements support.
|
// Props from Next, which includes NextLinkProps and all the things anchor elements support.
|
||||||
@@ -75,6 +76,46 @@ export const Link = React.forwardRef(function Link(
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A box used to contain a link overlay.
|
||||||
|
* It is used to create a clickable area that can contain other elements.
|
||||||
|
*/
|
||||||
|
export const LinkBox = React.forwardRef(function LinkBox(
|
||||||
|
props: React.BaseHTMLAttributes<HTMLDivElement>,
|
||||||
|
ref: React.Ref<HTMLDivElement>
|
||||||
|
) {
|
||||||
|
const { children, className, ...domProps } = props;
|
||||||
|
return (
|
||||||
|
<div ref={ref} {...domProps} className={tcls('elevate-link relative', className)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A link overlay that can be used to create a clickable area on top of other elements.
|
||||||
|
* It is used to create a link that covers the entire area of the element without encapsulating it in a link tag.
|
||||||
|
* This is useful to avoid nesting links inside links.
|
||||||
|
*/
|
||||||
|
export const LinkOverlay = React.forwardRef(function LinkOverlay(
|
||||||
|
props: LinkProps,
|
||||||
|
ref: React.Ref<HTMLAnchorElement>
|
||||||
|
) {
|
||||||
|
const { children, className, ...domProps } = props;
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
ref={ref}
|
||||||
|
{...domProps}
|
||||||
|
className={tcls(
|
||||||
|
'link-overlay static before:absolute before:top-0 before:left-0 before:z-10 before:h-full before:w-full',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a link is external, compared to an origin.
|
* Check if a link is external, compared to an origin.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { getAssetURL } from '@/lib/assets';
|
|||||||
import { filterOutNullable } from '@/lib/typescript';
|
import { filterOutNullable } from '@/lib/typescript';
|
||||||
import { getCacheTag } from '@gitbook/cache-tags';
|
import { getCacheTag } from '@gitbook/cache-tags';
|
||||||
import type { GitBookSiteContext } from '@v2/lib/context';
|
import type { GitBookSiteContext } from '@v2/lib/context';
|
||||||
import { getCloudflareContext } from '@v2/lib/data/cloudflare';
|
|
||||||
import { getResizedImageURL } from '@v2/lib/images';
|
import { getResizedImageURL } from '@v2/lib/images';
|
||||||
|
|
||||||
const googleFontsMap: { [fontName in CustomizationDefaultFont]: string } = {
|
const googleFontsMap: { [fontName in CustomizationDefaultFont]: string } = {
|
||||||
@@ -73,8 +72,12 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
|
|||||||
|
|
||||||
const fonts = (
|
const fonts = (
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
loadGoogleFont({ fontFamily, text: regularText, weight: 400 }),
|
getWithCache(`google-font:${fontFamily}:400`, () =>
|
||||||
loadGoogleFont({ fontFamily, text: boldText, weight: 700 }),
|
loadGoogleFont({ fontFamily, text: regularText, weight: 400 })
|
||||||
|
),
|
||||||
|
getWithCache(`google-font:${fontFamily}:700`, () =>
|
||||||
|
loadGoogleFont({ fontFamily, text: boldText, weight: 700 })
|
||||||
|
),
|
||||||
])
|
])
|
||||||
).filter(filterOutNullable);
|
).filter(filterOutNullable);
|
||||||
|
|
||||||
@@ -312,20 +315,14 @@ async function loadCustomFont(input: { url: string; weight: 400 | 700 }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch a resource from the function itself.
|
* Temporary function to log some data on Cloudflare.
|
||||||
* To avoid error with worker to worker requests in the same zone, we use the `WORKER_SELF_REFERENCE` binding.
|
* TODO: remove this when we found the issue
|
||||||
*/
|
*/
|
||||||
async function fetchSelf(url: string) {
|
function logOnCloudflareOnly(message: string) {
|
||||||
const cloudflare = getCloudflareContext();
|
if (process.env.DEBUG_CLOUDFLARE === 'true') {
|
||||||
if (cloudflare?.env.WORKER_SELF_REFERENCE) {
|
// biome-ignore lint/suspicious/noConsole: <explanation>
|
||||||
return await cloudflare.env.WORKER_SELF_REFERENCE.fetch(
|
console.log(message);
|
||||||
// `getAssetURL` can return a relative URL, so we need to make it absolute
|
|
||||||
// the URL doesn't matter, as we're using the worker-self-reference binding
|
|
||||||
new URL(url, 'https://worker-self-reference/')
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return await fetch(url);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -334,6 +331,9 @@ async function fetchSelf(url: string) {
|
|||||||
async function readImage(response: Response) {
|
async function readImage(response: Response) {
|
||||||
const contentType = response.headers.get('content-type');
|
const contentType = response.headers.get('content-type');
|
||||||
if (!contentType || !contentType.startsWith('image/')) {
|
if (!contentType || !contentType.startsWith('image/')) {
|
||||||
|
logOnCloudflareOnly(`Invalid content type: ${contentType},
|
||||||
|
status: ${response.status}
|
||||||
|
rayId: ${response.headers.get('cf-ray')}`);
|
||||||
throw new Error(`Invalid content type: ${contentType}`);
|
throw new Error(`Invalid content type: ${contentType}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,27 +342,34 @@ async function readImage(response: Response) {
|
|||||||
return `data:${contentType};base64,${base64}`;
|
return `data:${contentType};base64,${base64}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const staticImagesCache = new Map<string, string>();
|
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||||
|
const staticCache = new Map<string, any>();
|
||||||
|
|
||||||
|
// Do we need to limit the in-memory cache size? I think given the usage, we should be fine.
|
||||||
|
async function getWithCache<T>(key: string, fn: () => Promise<T>) {
|
||||||
|
const cached = staticCache.get(key) as T;
|
||||||
|
if (cached) {
|
||||||
|
return Promise.resolve(cached);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await fn();
|
||||||
|
staticCache.set(key, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read a static image and cache it in memory.
|
* Read a static image and cache it in memory.
|
||||||
*/
|
*/
|
||||||
async function readStaticImage(url: string) {
|
async function readStaticImage(url: string) {
|
||||||
const cached = staticImagesCache.get(url);
|
logOnCloudflareOnly(`Reading static image: ${url}, cache size: ${staticCache.size}`);
|
||||||
if (cached) {
|
return getWithCache(`static-image:${url}`, () => readSelfImage(url));
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
|
|
||||||
const image = await readSelfImage(url);
|
|
||||||
staticImagesCache.set(url, image);
|
|
||||||
return image;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read an image from GitBook itself.
|
* Read an image from GitBook itself.
|
||||||
*/
|
*/
|
||||||
async function readSelfImage(url: string) {
|
async function readSelfImage(url: string) {
|
||||||
const response = await fetchSelf(url);
|
const response = await fetch(url);
|
||||||
const image = await readImage(response);
|
const image = await readImage(response);
|
||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -458,12 +458,16 @@ const config: Config = {
|
|||||||
/**
|
/**
|
||||||
* Variant when a header is displayed.
|
* Variant when a header is displayed.
|
||||||
*/
|
*/
|
||||||
addVariant('site-header-none', 'html.site-header-none &');
|
addVariant('site-header-none', 'body:not(:has(#site-header:not(.mobile-only))) &');
|
||||||
addVariant('site-header', 'body:has(#site-header:not(.mobile-only)) &');
|
addVariant('site-header', 'body:has(#site-header:not(.mobile-only)) &');
|
||||||
addVariant('site-header-sections', [
|
addVariant('site-header-sections', [
|
||||||
'body:has(#site-header:not(.mobile-only) #sections) &',
|
'body:has(#site-header:not(.mobile-only) #sections) &',
|
||||||
'body:has(.page-no-toc):has(#site-header:not(.mobile-only) #variants) &',
|
'body:has(.page-no-toc):has(#site-header:not(.mobile-only) #variants) &',
|
||||||
]);
|
]);
|
||||||
|
addVariant(
|
||||||
|
'announcement',
|
||||||
|
'html:not(.announcement-hidden):has(#announcement-banner) &'
|
||||||
|
);
|
||||||
|
|
||||||
const customisationVariants = {
|
const customisationVariants = {
|
||||||
// Sidebar styles
|
// Sidebar styles
|
||||||
|
|||||||
@@ -312,6 +312,11 @@ function getSecurityHeaders(securities: OpenAPIOperationData['securities']): {
|
|||||||
[name]: 'YOUR_API_KEY',
|
[name]: 'YOUR_API_KEY',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case 'oauth2': {
|
||||||
|
return {
|
||||||
|
Authorization: 'Bearer YOUR_OAUTH2_TOKEN',
|
||||||
|
};
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export function OpenAPISpec(props: {
|
|||||||
|
|
||||||
const { operation } = data;
|
const { operation } = data;
|
||||||
|
|
||||||
const parameters = operation.parameters ?? [];
|
const parameters = deduplicateParameters(operation.parameters ?? []);
|
||||||
const parameterGroups = groupParameters(parameters, context);
|
const parameterGroups = groupParameters(parameters, context);
|
||||||
|
|
||||||
const securities = 'securities' in data ? data.securities : [];
|
const securities = 'securities' in data ? data.securities : [];
|
||||||
@@ -113,3 +113,23 @@ function getParameterGroupName(paramIn: string, context: OpenAPIClientContext):
|
|||||||
return paramIn;
|
return paramIn;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Deduplicate parameters by name and in.
|
||||||
|
* Some specs have both parameters define at path and operation level.
|
||||||
|
* We only want to display one of them.
|
||||||
|
*/
|
||||||
|
function deduplicateParameters(parameters: OpenAPI.Parameters): OpenAPI.Parameters {
|
||||||
|
const seen = new Set();
|
||||||
|
|
||||||
|
return parameters.filter((param) => {
|
||||||
|
const key = `${param.name}:${param.in}`;
|
||||||
|
|
||||||
|
if (seen.has(key)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
seen.add(key);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -400,7 +400,7 @@ describe('python code sample generator', () => {
|
|||||||
const output = generator?.generate(input);
|
const output = generator?.generate(input);
|
||||||
|
|
||||||
expect(output).toBe(
|
expect(output).toBe(
|
||||||
'import requests\n\nresponse = requests.get(\n "https://example.com/path",\n headers={"Content-Type":"application/x-www-form-urlencoded"},\n data={"key":"value"}\n)\n\ndata = response.json()'
|
'import requests\n\nresponse = requests.get(\n "https://example.com/path",\n headers={"Content-Type":"application/x-www-form-urlencoded"},\n data={\n "key": "value"\n }\n)\n\ndata = response.json()'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -422,7 +422,7 @@ describe('python code sample generator', () => {
|
|||||||
const output = generator?.generate(input);
|
const output = generator?.generate(input);
|
||||||
|
|
||||||
expect(output).toBe(
|
expect(output).toBe(
|
||||||
'import requests\n\nresponse = requests.get(\n "https://example.com/path",\n headers={"Content-Type":"application/json"},\n data=json.dumps({"key":"value","truethy":True,"falsey":False,"nullish":None})\n)\n\ndata = response.json()'
|
'import requests\n\nresponse = requests.get(\n "https://example.com/path",\n headers={"Content-Type":"application/json"},\n data=json.dumps({\n "key": "value",\n "truethy": True,\n "falsey": False,\n "nullish": None\n })\n)\n\ndata = response.json()'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -356,18 +356,22 @@ const BodyGenerators = {
|
|||||||
// Convert JSON to XML if needed
|
// Convert JSON to XML if needed
|
||||||
body = JSON.stringify(convertBodyToXML(body));
|
body = JSON.stringify(convertBodyToXML(body));
|
||||||
} else {
|
} else {
|
||||||
body = stringifyOpenAPI(body, (_key, value) => {
|
body = stringifyOpenAPI(
|
||||||
switch (value) {
|
body,
|
||||||
case true:
|
(_key, value) => {
|
||||||
return '$$__TRUE__$$';
|
switch (value) {
|
||||||
case false:
|
case true:
|
||||||
return '$$__FALSE__$$';
|
return '$$__TRUE__$$';
|
||||||
case null:
|
case false:
|
||||||
return '$$__NULL__$$';
|
return '$$__FALSE__$$';
|
||||||
default:
|
case null:
|
||||||
return value;
|
return '$$__NULL__$$';
|
||||||
}
|
default:
|
||||||
})
|
return value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
2
|
||||||
|
)
|
||||||
.replaceAll('"$$__TRUE__$$"', 'True')
|
.replaceAll('"$$__TRUE__$$"', 'True')
|
||||||
.replaceAll('"$$__FALSE__$$"', 'False')
|
.replaceAll('"$$__FALSE__$$"', 'False')
|
||||||
.replaceAll('"$$__NULL__$$"', 'None');
|
.replaceAll('"$$__NULL__$$"', 'None');
|
||||||
|
|||||||
@@ -1017,4 +1017,24 @@ describe('generateSchemaExample', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('handles deprecated properties', () => {
|
||||||
|
expect(
|
||||||
|
generateSchemaExample({
|
||||||
|
type: 'object',
|
||||||
|
deprecated: true,
|
||||||
|
})
|
||||||
|
).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handle nested deprecated properties', () => {
|
||||||
|
expect(
|
||||||
|
generateSchemaExample({
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
deprecated: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).toBeUndefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ const getExampleFromSchema = (
|
|||||||
const makeUpRandomData = !!options?.emptyString;
|
const makeUpRandomData = !!options?.emptyString;
|
||||||
|
|
||||||
// If the property is deprecated we don't show it in examples.
|
// If the property is deprecated we don't show it in examples.
|
||||||
if (schema.deprecated) {
|
if (schema.deprecated || (schema.type === 'array' && schema.items?.deprecated)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user